From 60e7f1dcd120c5512506050611d99c3455cbd912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Wed, 22 Jun 2022 20:39:18 +0200 Subject: [PATCH 01/54] Rename `viewer_user` to `viewer` (#134294) --- packages/kbn-test/src/index.ts | 2 + .../src/kbn_client/kbn_client_requester.ts | 3 +- .../kbn_client/kbn_client_requester_error.ts | 21 +++ x-pack/plugins/apm/dev_docs/local_setup.md | 4 +- .../integration/power_user/no_data_screen.ts | 4 +- .../apm/ftr_e2e/cypress/support/commands.ts | 4 +- .../plugins/apm/scripts/create_apm_users.js | 4 +- .../create_apm_users/create_apm_users.ts | 4 +- .../e2e/journeys/data_view_permissions.ts | 2 +- .../monitor_management_enablement.journey.ts | 2 +- .../read_only_user/monitor_management.ts | 2 +- .../synthetics/e2e/page_objects/login.tsx | 2 +- .../ux/e2e/journeys/core_web_vitals.ts | 2 +- .../ux/e2e/journeys/url_ux_query.journey.ts | 2 +- .../ux/e2e/journeys/ux_js_errors.journey.ts | 2 +- .../common/authentication.ts | 144 ++++++++++++------ .../test/apm_api_integration/common/config.ts | 122 ++++++++++----- .../settings/agent_keys/agent_keys.spec.ts | 4 +- 18 files changed, 225 insertions(+), 105 deletions(-) create mode 100644 packages/kbn-test/src/kbn_client/kbn_client_requester_error.ts diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index c9f0e67c558f1..ea4cfb0cd0fba 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -15,6 +15,8 @@ import { // @ts-ignore not typed yet } from './functional_tests/cli'; +export { KbnClientRequesterError } from './kbn_client/kbn_client_requester_error'; + // @internal export { runTestsCli, processRunTestsCliOptions, startServersCli, processStartServersCliOptions }; diff --git a/packages/kbn-test/src/kbn_client/kbn_client_requester.ts b/packages/kbn-test/src/kbn_client/kbn_client_requester.ts index a57515474faf9..36a007c1c0d1c 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_requester.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_requester.ts @@ -13,6 +13,7 @@ import Qs from 'querystring'; import Axios, { AxiosResponse, ResponseType } from 'axios'; import { isAxiosRequestError, isAxiosResponseError } from '@kbn/dev-utils'; import { ToolingLog } from '@kbn/tooling-log'; +import { KbnClientRequesterError } from './kbn_client_requester_error'; const isConcliftOnGetError = (error: any) => { return ( @@ -166,7 +167,7 @@ export class KbnClientRequester { continue; } - throw new Error(`${errorMessage} -- and ran out of retries`); + throw new KbnClientRequesterError(`${errorMessage} -- and ran out of retries`, error); } } } diff --git a/packages/kbn-test/src/kbn_client/kbn_client_requester_error.ts b/packages/kbn-test/src/kbn_client/kbn_client_requester_error.ts new file mode 100644 index 0000000000000..d338b24cd16ad --- /dev/null +++ b/packages/kbn-test/src/kbn_client/kbn_client_requester_error.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { AxiosError } from 'axios'; + +export class KbnClientRequesterError extends Error { + axiosError?: AxiosError; + constructor(message: string, error: unknown) { + super(message); + this.name = 'KbnClientRequesterError'; + + if (error instanceof AxiosError) { + this.axiosError = error; + } + } +} diff --git a/x-pack/plugins/apm/dev_docs/local_setup.md b/x-pack/plugins/apm/dev_docs/local_setup.md index 42aaf686dac5b..24a8db44a3cce 100644 --- a/x-pack/plugins/apm/dev_docs/local_setup.md +++ b/x-pack/plugins/apm/dev_docs/local_setup.md @@ -90,8 +90,8 @@ node x-pack/plugins/apm/scripts/create_apm_users.js --username admin --password This will create: -- **viewer_user**: User with `viewer` role (read-only) -- **editor_user**: User with `editor` role (read/write) +- **viewer**: User with `viewer` role (read-only) +- **editor**: User with `editor` role (read/write) # Debugging Elasticsearch queries diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/no_data_screen.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/no_data_screen.ts index 65591bf991ab8..c7f33301a5dfc 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/no_data_screen.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/no_data_screen.ts @@ -29,7 +29,7 @@ describe('No data screen', () => { headers: { 'kbn-xsrf': true, }, - auth: { user: 'editor_user', pass: 'changeme' }, + auth: { user: 'editor', pass: 'changeme' }, }); }); @@ -57,7 +57,7 @@ describe('No data screen', () => { metric: '', }, headers: { 'kbn-xsrf': true }, - auth: { user: 'editor_user', pass: 'changeme' }, + auth: { user: 'editor', pass: 'changeme' }, }); }); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts index bf0be24353847..94c1f44cbcffc 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts @@ -11,11 +11,11 @@ import moment from 'moment'; import { AXE_CONFIG, AXE_OPTIONS } from '@kbn/axe-config'; Cypress.Commands.add('loginAsViewerUser', () => { - cy.loginAs({ username: 'viewer_user', password: 'changeme' }); + cy.loginAs({ username: 'viewer', password: 'changeme' }); }); Cypress.Commands.add('loginAsEditorUser', () => { - cy.loginAs({ username: 'editor_user', password: 'changeme' }); + cy.loginAs({ username: 'editor', password: 'changeme' }); }); Cypress.Commands.add( diff --git a/x-pack/plugins/apm/scripts/create_apm_users.js b/x-pack/plugins/apm/scripts/create_apm_users.js index 37a70c70ef3b0..8cef6ebb6c7ae 100644 --- a/x-pack/plugins/apm/scripts/create_apm_users.js +++ b/x-pack/plugins/apm/scripts/create_apm_users.js @@ -7,8 +7,8 @@ /* * This script will create two users - * - editor_user - * - viewer_user + * - editor + * - viewer * * Usage: node create-apm-users.js ******************************/ diff --git a/x-pack/plugins/apm/scripts/create_apm_users/create_apm_users.ts b/x-pack/plugins/apm/scripts/create_apm_users/create_apm_users.ts index f7d0ea2e78ed8..7532392c9a8b4 100644 --- a/x-pack/plugins/apm/scripts/create_apm_users/create_apm_users.ts +++ b/x-pack/plugins/apm/scripts/create_apm_users/create_apm_users.ts @@ -42,8 +42,8 @@ export async function createApmUsers({ // user definitions const users = [ - { username: 'viewer_user', roles: ['viewer'] }, - { username: 'editor_user', roles: ['editor'] }, + { username: 'viewer', roles: ['viewer'] }, + { username: 'editor', roles: ['editor'] }, ]; // create users diff --git a/x-pack/plugins/synthetics/e2e/journeys/data_view_permissions.ts b/x-pack/plugins/synthetics/e2e/journeys/data_view_permissions.ts index 10e027f249104..b265da7a3f0d6 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/data_view_permissions.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/data_view_permissions.ts @@ -38,7 +38,7 @@ journey('DataViewPermissions', async ({ page, params }) => { await page.goto(`${baseUrl}?${queryParams}`, { waitUntil: 'networkidle', }); - await login.loginToKibana('viewer_user', 'changeme'); + await login.loginToKibana('viewer', 'changeme'); }); step('Click explore data button', async () => { diff --git a/x-pack/plugins/synthetics/e2e/journeys/monitor_management_enablement.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/monitor_management_enablement.journey.ts index b7308ece8af21..5d22fe8491579 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/monitor_management_enablement.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/monitor_management_enablement.journey.ts @@ -57,7 +57,7 @@ journey( }); step('login to Kibana', async () => { - await uptime.loginToKibana('editor_user', 'changeme'); + await uptime.loginToKibana('editor', 'changeme'); const invalid = await page.locator( `text=Username or password is incorrect. Please try again.` ); diff --git a/x-pack/plugins/synthetics/e2e/journeys/read_only_user/monitor_management.ts b/x-pack/plugins/synthetics/e2e/journeys/read_only_user/monitor_management.ts index 33698961951da..677983ca9260b 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/read_only_user/monitor_management.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/read_only_user/monitor_management.ts @@ -23,7 +23,7 @@ journey( }); step('login to Kibana', async () => { - await uptime.loginToKibana('viewer_user', 'changeme'); + await uptime.loginToKibana('viewer', 'changeme'); }); step('Adding monitor is disabled', async () => { diff --git a/x-pack/plugins/synthetics/e2e/page_objects/login.tsx b/x-pack/plugins/synthetics/e2e/page_objects/login.tsx index ae52bb45ddb84..ee0fe0f596b98 100644 --- a/x-pack/plugins/synthetics/e2e/page_objects/login.tsx +++ b/x-pack/plugins/synthetics/e2e/page_objects/login.tsx @@ -24,7 +24,7 @@ export function loginPageProvider({ await page.waitForTimeout(5 * 1000); } }, - async loginToKibana(usernameT?: string, passwordT?: string) { + async loginToKibana(usernameT?: 'editor' | 'viewer', passwordT?: string) { if (isRemote) { await page.click('text="Log in with Elasticsearch"'); } diff --git a/x-pack/plugins/ux/e2e/journeys/core_web_vitals.ts b/x-pack/plugins/ux/e2e/journeys/core_web_vitals.ts index 4138027a6b356..afbec5c055cf1 100644 --- a/x-pack/plugins/ux/e2e/journeys/core_web_vitals.ts +++ b/x-pack/plugins/ux/e2e/journeys/core_web_vitals.ts @@ -29,7 +29,7 @@ journey('Core Web Vitals', async ({ page, params }) => { }); await loginToKibana({ page, - user: { username: 'viewer_user', password: 'changeme' }, + user: { username: 'viewer', password: 'changeme' }, }); }); diff --git a/x-pack/plugins/ux/e2e/journeys/url_ux_query.journey.ts b/x-pack/plugins/ux/e2e/journeys/url_ux_query.journey.ts index f5e8fd19a9557..1762cd22a1186 100644 --- a/x-pack/plugins/ux/e2e/journeys/url_ux_query.journey.ts +++ b/x-pack/plugins/ux/e2e/journeys/url_ux_query.journey.ts @@ -29,7 +29,7 @@ journey('UX URL Query', async ({ page, params }) => { }); await loginToKibana({ page, - user: { username: 'viewer_user', password: 'changeme' }, + user: { username: 'viewer', password: 'changeme' }, }); }); diff --git a/x-pack/plugins/ux/e2e/journeys/ux_js_errors.journey.ts b/x-pack/plugins/ux/e2e/journeys/ux_js_errors.journey.ts index eb61a6e446013..8e124d8a5e4b7 100644 --- a/x-pack/plugins/ux/e2e/journeys/ux_js_errors.journey.ts +++ b/x-pack/plugins/ux/e2e/journeys/ux_js_errors.journey.ts @@ -34,7 +34,7 @@ journey('UX JsErrors', async ({ page, params }) => { }); await loginToKibana({ page, - user: { username: 'viewer_user', password: 'changeme' }, + user: { username: 'viewer', password: 'changeme' }, }); }); diff --git a/x-pack/test/apm_api_integration/common/authentication.ts b/x-pack/test/apm_api_integration/common/authentication.ts index 288f483c8c005..28dffac7f80ca 100644 --- a/x-pack/test/apm_api_integration/common/authentication.ts +++ b/x-pack/test/apm_api_integration/common/authentication.ts @@ -7,25 +7,33 @@ import { Client } from '@elastic/elasticsearch'; import { PrivilegeType } from '@kbn/apm-plugin/common/privilege_type'; +import { ToolingLog } from '@kbn/tooling-log'; +import { omit } from 'lodash'; +import { KbnClientRequesterError } from '@kbn/test'; +import { AxiosError } from 'axios'; import { SecurityServiceProvider } from '../../../../test/common/services/security'; type SecurityService = Awaited>; -export enum ApmUser { +export enum ApmUsername { noAccessUser = 'no_access_user', - viewerUser = 'viewer_user', - editorUser = 'editor_user', + viewerUser = 'viewer', + editorUser = 'editor', apmAnnotationsWriteUser = 'apm_annotations_write_user', apmReadUserWithoutMlAccess = 'apm_read_user_without_ml_access', apmManageOwnAgentKeys = 'apm_manage_own_agent_keys', apmManageOwnAndCreateAgentKeys = 'apm_manage_own_and_create_agent_keys', } -const roles = { - [ApmUser.noAccessUser]: {}, - [ApmUser.viewerUser]: {}, - [ApmUser.editorUser]: {}, - [ApmUser.apmReadUserWithoutMlAccess]: { +export enum ApmCustomRolename { + apmReadUserWithoutMlAccess = 'apm_read_user_without_ml_access', + apmAnnotationsWriteUser = 'apm_annotations_write_user', + apmManageOwnAgentKeys = 'apm_manage_own_agent_keys', + apmManageOwnAndCreateAgentKeys = 'apm_manage_own_and_create_agent_keys', +} + +const customRoles = { + [ApmCustomRolename.apmReadUserWithoutMlAccess]: { elasticsearch: { cluster: [], indices: [ @@ -43,7 +51,7 @@ const roles = { }, ], }, - [ApmUser.apmAnnotationsWriteUser]: { + [ApmCustomRolename.apmAnnotationsWriteUser]: { elasticsearch: { cluster: [], indices: [ @@ -61,12 +69,12 @@ const roles = { ], }, }, - [ApmUser.apmManageOwnAgentKeys]: { + [ApmCustomRolename.apmManageOwnAgentKeys]: { elasticsearch: { cluster: ['manage_own_api_key'], }, }, - [ApmUser.apmManageOwnAndCreateAgentKeys]: { + [ApmCustomRolename.apmManageOwnAndCreateAgentKeys]: { applications: [ { application: 'apm', @@ -77,55 +85,101 @@ const roles = { }, }; -const users = { - [ApmUser.noAccessUser]: { - roles: [], - }, - [ApmUser.viewerUser]: { - roles: ['viewer'], +const users: Record< + ApmUsername, + { builtInRoleNames?: string[]; customRoleNames?: ApmCustomRolename[] } +> = { + [ApmUsername.noAccessUser]: {}, + [ApmUsername.viewerUser]: { + builtInRoleNames: ['viewer'], }, - [ApmUser.editorUser]: { - roles: ['editor'], + [ApmUsername.editorUser]: { + builtInRoleNames: ['editor'], }, - [ApmUser.apmReadUserWithoutMlAccess]: { - roles: [ApmUser.apmReadUserWithoutMlAccess], + [ApmUsername.apmReadUserWithoutMlAccess]: { + customRoleNames: [ApmCustomRolename.apmReadUserWithoutMlAccess], }, - [ApmUser.apmAnnotationsWriteUser]: { - roles: ['editor', ApmUser.apmAnnotationsWriteUser], + [ApmUsername.apmAnnotationsWriteUser]: { + builtInRoleNames: ['editor'], + customRoleNames: [ApmCustomRolename.apmAnnotationsWriteUser], }, - [ApmUser.apmManageOwnAgentKeys]: { - roles: ['editor', ApmUser.apmManageOwnAgentKeys], + [ApmUsername.apmManageOwnAgentKeys]: { + builtInRoleNames: ['editor'], + customRoleNames: [ApmCustomRolename.apmManageOwnAgentKeys], }, - [ApmUser.apmManageOwnAndCreateAgentKeys]: { - roles: ['editor', ApmUser.apmManageOwnAgentKeys, ApmUser.apmManageOwnAndCreateAgentKeys], + [ApmUsername.apmManageOwnAndCreateAgentKeys]: { + builtInRoleNames: ['editor'], + customRoleNames: [ + ApmCustomRolename.apmManageOwnAgentKeys, + ApmCustomRolename.apmManageOwnAndCreateAgentKeys, + ], }, }; -export async function createApmUser(security: SecurityService, apmUser: ApmUser, es: Client) { - const role = roles[apmUser]; - const user = users[apmUser]; +function logErrorResponse(logger: ToolingLog, e: Error) { + if (e instanceof KbnClientRequesterError) { + logger.error(`KbnClientRequesterError: ${JSON.stringify(e.axiosError?.response?.data)}`); + } else if (e instanceof AxiosError) { + logger.error(`AxiosError: ${JSON.stringify(e.response?.data)}`); + } else { + logger.error(`Unknown error: ${e.constructor.name}`); + } +} + +export async function createApmUser({ + username, + security, + es, + logger, +}: { + username: ApmUsername; + security: SecurityService; + es: Client; + logger: ToolingLog; +}) { + const user = users[username]; - if (!role || !user) { - throw new Error(`No configuration found for ${apmUser}`); + if (!user) { + throw new Error(`No configuration found for ${username}`); } - if ('applications' in role) { - // Add application privileges with es client as they are not supported by - // security.user.create. They are preserved when updating the role below - await es.security.putRole({ - name: apmUser, - body: role, + const { builtInRoleNames = [], customRoleNames = [] } = user; + + try { + // create custom roles + await Promise.all( + customRoleNames.map(async (roleName) => createCustomRole({ roleName, security, es })) + ); + + // create user + await security.user.create(username, { + full_name: username, + password: APM_TEST_PASSWORD, + roles: [...builtInRoleNames, ...customRoleNames], }); - delete (role as any).applications; + } catch (e) { + logErrorResponse(logger, e); + throw e; } +} - await security.role.create(apmUser, role); +async function createCustomRole({ + roleName, + security, + es, +}: { + roleName: ApmCustomRolename; + security: SecurityService; + es: Client; +}) { + const role = customRoles[roleName]; - await security.user.create(apmUser, { - full_name: apmUser, - password: APM_TEST_PASSWORD, - roles: user.roles, - }); + // Add application privileges with es client as they are not supported by + // security.user.create. They are preserved when updating the role below + if ('applications' in role) { + await es.security.putRole({ name: roleName, body: role }); + } + await security.role.create(roleName, omit(role, 'applications')); } export const APM_TEST_PASSWORD = 'changeme'; diff --git a/x-pack/test/apm_api_integration/common/config.ts b/x-pack/test/apm_api_integration/common/config.ts index 4b56ad52c2e3f..3c9e99d645c7b 100644 --- a/x-pack/test/apm_api_integration/common/config.ts +++ b/x-pack/test/apm_api_integration/common/config.ts @@ -9,9 +9,10 @@ import { FtrConfigProviderContext } from '@kbn/test'; import supertest from 'supertest'; import { format, UrlObject } from 'url'; import { Client } from '@elastic/elasticsearch'; +import { ToolingLog } from '@kbn/tooling-log'; import { SecurityServiceProvider } from '../../../../test/common/services/security'; import { InheritedFtrProviderContext, InheritedServices } from './ftr_provider_context'; -import { createApmUser, APM_TEST_PASSWORD, ApmUser } from './authentication'; +import { createApmUser, APM_TEST_PASSWORD, ApmUsername } from './authentication'; import { APMFtrConfigName } from '../configs'; import { createApmApiClient } from './apm_api_supertest'; import { RegistryProvider } from './registry'; @@ -26,34 +27,42 @@ export interface ApmFtrConfig { type SecurityService = Awaited>; -function getLegacySupertestClient(kibanaServer: UrlObject, apmUser: ApmUser) { +function getLegacySupertestClient(kibanaServer: UrlObject, username: ApmUsername) { return async (context: InheritedFtrProviderContext) => { const security = context.getService('security'); const es = context.getService('es'); + const logger = context.getService('log'); await security.init(); - await createApmUser(security, apmUser, es); + await createApmUser({ security, username, es, logger }); const url = format({ ...kibanaServer, - auth: `${apmUser}:${APM_TEST_PASSWORD}`, + auth: `${username}:${APM_TEST_PASSWORD}`, }); return supertest(url); }; } -async function getApmApiClient( - kibanaServer: UrlObject, - security: SecurityService, - apmUser: ApmUser, - es: Client -) { - await createApmUser(security, apmUser, es); +async function getApmApiClient({ + kibanaServer, + security, + username, + es, + logger, +}: { + kibanaServer: UrlObject; + security: SecurityService; + username: ApmUsername; + es: Client; + logger: ToolingLog; +}) { + await createApmUser({ security, username, es, logger }); const url = format({ ...kibanaServer, - auth: `${apmUser}:${APM_TEST_PASSWORD}`, + auth: `${username}:${APM_TEST_PASSWORD}`, }); return createApmApiClient(supertest(url)); @@ -85,50 +94,83 @@ export function createTestConfig(config: ApmFtrConfig) { apmApiClient: async (context: InheritedFtrProviderContext) => { const security = context.getService('security'); const es = context.getService('es'); + const logger = context.getService('log'); + await security.init(); return { - noAccessUser: await getApmApiClient(servers.kibana, security, ApmUser.noAccessUser, es), - readUser: await getApmApiClient(servers.kibana, security, ApmUser.viewerUser, es), - writeUser: await getApmApiClient(servers.kibana, security, ApmUser.editorUser, es), - annotationWriterUser: await getApmApiClient( - servers.kibana, + noAccessUser: await getApmApiClient({ + kibanaServer: servers.kibana, + security, + username: ApmUsername.noAccessUser, + es, + logger, + }), + readUser: await getApmApiClient({ + kibanaServer: servers.kibana, + security, + username: ApmUsername.viewerUser, + es, + logger, + }), + writeUser: await getApmApiClient({ + kibanaServer: servers.kibana, security, - ApmUser.apmAnnotationsWriteUser, - es - ), - noMlAccessUser: await getApmApiClient( - servers.kibana, + username: ApmUsername.editorUser, + es, + logger, + }), + annotationWriterUser: await getApmApiClient({ + kibanaServer: servers.kibana, security, - ApmUser.apmReadUserWithoutMlAccess, - es - ), - manageOwnAgentKeysUser: await getApmApiClient( - servers.kibana, + username: ApmUsername.apmAnnotationsWriteUser, + es, + logger, + }), + noMlAccessUser: await getApmApiClient({ + kibanaServer: servers.kibana, security, - ApmUser.apmManageOwnAgentKeys, - es - ), - createAndAllAgentKeysUser: await getApmApiClient( - servers.kibana, + username: ApmUsername.apmReadUserWithoutMlAccess, + es, + logger, + }), + manageOwnAgentKeysUser: await getApmApiClient({ + kibanaServer: servers.kibana, security, - ApmUser.apmManageOwnAndCreateAgentKeys, - es - ), + username: ApmUsername.apmManageOwnAgentKeys, + es, + logger, + }), + createAndAllAgentKeysUser: await getApmApiClient({ + kibanaServer: servers.kibana, + security, + username: ApmUsername.apmManageOwnAndCreateAgentKeys, + es, + logger, + }), }; }, ml: MachineLearningAPIProvider, // legacy clients - legacySupertestAsNoAccessUser: getLegacySupertestClient(kibanaServer, ApmUser.noAccessUser), - legacySupertestAsApmReadUser: getLegacySupertestClient(kibanaServer, ApmUser.viewerUser), - legacySupertestAsApmWriteUser: getLegacySupertestClient(kibanaServer, ApmUser.editorUser), + legacySupertestAsNoAccessUser: getLegacySupertestClient( + kibanaServer, + ApmUsername.noAccessUser + ), + legacySupertestAsApmReadUser: getLegacySupertestClient( + kibanaServer, + ApmUsername.viewerUser + ), + legacySupertestAsApmWriteUser: getLegacySupertestClient( + kibanaServer, + ApmUsername.editorUser + ), legacySupertestAsApmAnnotationsWriteUser: getLegacySupertestClient( kibanaServer, - ApmUser.apmAnnotationsWriteUser + ApmUsername.apmAnnotationsWriteUser ), legacySupertestAsApmReadUserWithoutMlAccess: getLegacySupertestClient( kibanaServer, - ApmUser.apmReadUserWithoutMlAccess + ApmUsername.apmReadUserWithoutMlAccess ), }, junit: { diff --git a/x-pack/test/apm_api_integration/tests/settings/agent_keys/agent_keys.spec.ts b/x-pack/test/apm_api_integration/tests/settings/agent_keys/agent_keys.spec.ts index 4a32ef4a023a6..2f607cecb75bb 100644 --- a/x-pack/test/apm_api_integration/tests/settings/agent_keys/agent_keys.spec.ts +++ b/x-pack/test/apm_api_integration/tests/settings/agent_keys/agent_keys.spec.ts @@ -9,7 +9,7 @@ import { first } from 'lodash'; import { PrivilegeType } from '@kbn/apm-plugin/common/privilege_type'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { ApmApiError, ApmApiSupertest } from '../../../common/apm_api_supertest'; -import { ApmUser } from '../../../common/authentication'; +import { ApmUsername } from '../../../common/authentication'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); @@ -92,7 +92,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { () => { afterEach(async () => { await esClient.security.invalidateApiKey({ - username: ApmUser.apmManageOwnAndCreateAgentKeys, + username: ApmUsername.apmManageOwnAndCreateAgentKeys, }); }); From 16c2c717f121c6933b02daf9bce13a1fecd97c36 Mon Sep 17 00:00:00 2001 From: Kurt Date: Wed, 22 Jun 2022 16:20:24 -0400 Subject: [PATCH 02/54] Changing to pass a new Set to setState to fix potential no-op (#134936) --- .../edit_role/privileges/kibana/feature_table/feature_table.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx index 666cd39dc8cd7..a506f8675bc11 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx @@ -242,7 +242,7 @@ export class FeatureTable extends Component { } this.setState({ - expandedPrivilegeControls: this.state.expandedPrivilegeControls, + expandedPrivilegeControls: new Set([...this.state.expandedPrivilegeControls]), }); }} > From 81e820825870c6e095cacc0431914b452c3a5e34 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Wed, 22 Jun 2022 13:25:59 -0700 Subject: [PATCH 03/54] [DOCS] Add prerequisites to get rule, find rule, get rule health APIs (#134865) --- docs/api/alerting.asciidoc | 6 +- docs/api/alerting/create_rule.asciidoc | 9 ++- docs/api/alerting/delete_rule.asciidoc | 8 +- docs/api/alerting/find_rules.asciidoc | 101 +++++++++++++++---------- docs/api/alerting/get_rules.asciidoc | 32 +++++--- docs/api/alerting/health.asciidoc | 101 ++++++++++++------------- docs/api/alerting/update_rule.asciidoc | 7 +- 7 files changed, 151 insertions(+), 113 deletions(-) diff --git a/docs/api/alerting.asciidoc b/docs/api/alerting.asciidoc index fd5a23886cc5a..782b5aaaa77c7 100644 --- a/docs/api/alerting.asciidoc +++ b/docs/api/alerting.asciidoc @@ -33,9 +33,10 @@ For deprecated APIs, refer to <>. include::alerting/create_rule.asciidoc[leveloffset=+1] include::alerting/delete_rule.asciidoc[leveloffset=+1] +include::alerting/find_rules.asciidoc[leveloffset=+1] +include::alerting/health.asciidoc[leveloffset=+1] +include::alerting/get_rules.asciidoc[leveloffset=+1] include::alerting/update_rule.asciidoc[leveloffset=+1] -include::alerting/get_rules.asciidoc[] -include::alerting/find_rules.asciidoc[] include::alerting/list_rule_types.asciidoc[] include::alerting/enable_rule.asciidoc[] include::alerting/disable_rule.asciidoc[] @@ -43,5 +44,4 @@ include::alerting/mute_all_alerts.asciidoc[] include::alerting/mute_alert.asciidoc[] include::alerting/unmute_all_alerts.asciidoc[] include::alerting/unmute_alert.asciidoc[] -include::alerting/health.asciidoc[] include::alerting/legacy/index.asciidoc[] diff --git a/docs/api/alerting/create_rule.asciidoc b/docs/api/alerting/create_rule.asciidoc index 0b219ad00ebcb..86538499be010 100644 --- a/docs/api/alerting/create_rule.asciidoc +++ b/docs/api/alerting/create_rule.asciidoc @@ -16,10 +16,11 @@ Create {kib} rules. === {api-prereq-title} -You must have `all` privileges for the *Management* > *Stack Rules* feature or -for the *{ml-app}*, *{observability}*, or *Security* features, depending on the -`consumer` and `rule_type_id` of the rule you're creating. If the rule has -`actions`, you must also have `read` privileges for the *Management* > +You must have `all` privileges for the appropriate {kib} features, depending on +the `consumer` and `rule_type_id` of the rules you're creating. For example, the +*Management* > *Stack Rules* feature, *Analytics* > *Discover* and *{ml-app}* +features, *{observability}*, and *Security* features. If the rule has `actions`, +you must also have `read` privileges for the *Management* > *Actions and Connectors* feature. For more details, refer to <>. diff --git a/docs/api/alerting/delete_rule.asciidoc b/docs/api/alerting/delete_rule.asciidoc index 537b93872059c..12b07c5bb0f12 100644 --- a/docs/api/alerting/delete_rule.asciidoc +++ b/docs/api/alerting/delete_rule.asciidoc @@ -17,9 +17,11 @@ WARNING: After you delete a rule, you cannot recover it. === {api-prereq-title} -You must have `all` privileges for the *Management* > *Stack Rules* feature or -for the *{ml-app}*, *{observability}*, or *Security* features, depending on the -`consumer` and `rule_type_id` of the rule you're deleting. +You must have `all` privileges for the appropriate {kib} features, depending on +the `consumer` and `rule_type_id` of the rule you're deleting. For example, the +*Management* > *Stack Rules* feature, *Analytics* > *Discover* or *{ml-app}* +features, *{observability}*, or *Security* features. For more details, refer to +<>. [[delete-rule-api-path-params]] === {api-path-parms-title} diff --git a/docs/api/alerting/find_rules.asciidoc b/docs/api/alerting/find_rules.asciidoc index 48c4bb25e0eea..13b39db3f280b 100644 --- a/docs/api/alerting/find_rules.asciidoc +++ b/docs/api/alerting/find_rules.asciidoc @@ -1,77 +1,101 @@ [[find-rules-api]] -=== Find rules API +== Find rules API ++++ Find rules ++++ Retrieve a paginated set of rules based on condition. -NOTE: As rules change in {kib}, the results on each page of the response also -change. Use the find API for traditional paginated results, but avoid using it to export large amounts of data. - [[find-rules-api-request]] -==== Request +=== {api-request-title} `GET :/api/alerting/rules/_find` `GET :/s//api/alerting/rules/_find` +=== {api-prereq-title} + +You must have `read` privileges for the appropriate {kib} features, depending on +the `consumer` and `rule_type_id` of the rules you're seeking. For example, the +*Management* > *Stack Rules* feature, *Analytics* > *Discover* and *{ml-app}* +features, *{observability}*, and *Security* features. To find rules associated +with the *{stack-monitor-app}*, use the `monitoring_user` built-in role. + +For more details, refer to <>. + +=== {api-description-title} + +As rules change in {kib}, the results on each page of the response also change. +Use the find API for traditional paginated results, but avoid using it to export +large amounts of data. + +NOTE: Rule `params` are stored as a {ref}/flattened.html[flattened field type] +and analyzed as keywords. + [[find-rules-api-path-params]] -==== Path parameters +=== {api-path-parms-title} `space_id`:: - (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. +(Optional, string) An identifier for the space. If `space_id` is not provided in +the URL, the default space is used. [[find-rules-api-query-params]] -==== Query Parameters +=== {api-query-parms-title} -NOTE: Rule `params` are stored as a {ref}/flattened.html[flattened field type] and analyzed as keywords. +`default_search_operator`:: +(Optional, string) The operator to use for the `simple_query_string`. The +default is 'OR'. -`per_page`:: - (Optional, number) The number of rules to return per page. +`fields`:: +(Optional, array of strings) The fields to return in the `attributes` key of the +response. + +`filter`:: +(Optional, string) A <> string that you filter with an +attribute from your saved object. It should look like +`savedObjectType.attributes.title: "myTitle"`. However, If you used a direct +attribute of a saved object, such as `updatedAt`, you will have to define your +filter, for example, `savedObjectType.updatedAt > 2018-12-22`. + +`has_reference`:: +(Optional, object) Filters the rules that have a relation with the reference +objects with the specific "type" and "ID". `page`:: - (Optional, number) The page number. +(Optional, number) The page number. -`search`:: - (Optional, string) An Elasticsearch {ref}/query-dsl-simple-query-string-query.html[simple_query_string] query that filters the rules in the response. +`per_page`:: +(Optional, number) The number of rules to return per page. -`default_search_operator`:: - (Optional, string) The operator to use for the `simple_query_string`. The default is 'OR'. +`search`:: +(Optional, string) An {es} +{ref}/query-dsl-simple-query-string-query.html[simple_query_string] query that +filters the rules in the response. `search_fields`:: - (Optional, array|string) The fields to perform the `simple_query_string` parsed query against. - -`fields`:: - (Optional, array of strings) The fields to return in the `attributes` key of the response. +(Optional, array or string) The fields to perform the `simple_query_string` +parsed query against. `sort_field`:: - (Optional, string) Sorts the response. Could be a rule field returned in the `attributes` key of the response. +(Optional, string) Sorts the response. Could be a rule field returned in the +`attributes` key of the response. `sort_order`:: - (Optional, string) Sort direction, either `asc` or `desc`. - -`has_reference`:: - (Optional, object) Filters the rules that have a relation with the reference objects with the specific "type" and "ID". - -`filter`:: - (Optional, string) A <> string that you filter with an attribute from your saved object. - It should look like savedObjectType.attributes.title: "myTitle". However, If you used a direct attribute of a saved object, such as `updatedAt`, - you will have to define your filter, for example, savedObjectType.updatedAt > 2018-12-22. +(Optional, string) Sort direction, either `asc` or `desc`. [[find-rules-api-request-codes]] -==== Response code +=== {api-response-codes-title} `200`:: - Indicates a successful call. +Indicates a successful call. -==== Examples +=== {api-examples-title} Find rules with names that start with `my`: [source,sh] -------------------------------------------------- -$ curl -X GET api/alerting/rules/_find?search_fields=name&search=my* +GET api/alerting/rules/_find?search_fields=name&search=my* -------------------------------------------------- // KIBANA @@ -110,18 +134,19 @@ The API returns the following: "scheduled_task_id": "0b092d90-6b62-11eb-9e0d-85d233e3ee35", "execution_status": { "last_execution_date": "2021-02-10T17:55:14.262Z", - "status": "ok" + "status": "ok", + "last_duration": 384 } - }, + } ] } -------------------------------------------------- -For parameters that accept multiple values (e.g. `fields`), repeat the +For parameters that accept multiple values (such as `fields`), repeat the query parameter for each value: [source,sh] -------------------------------------------------- -$ curl -X GET api/alerting/rules/_find?fields=id&fields=name +GET api/alerting/rules/_find?fields=id&fields=name -------------------------------------------------- // KIBANA diff --git a/docs/api/alerting/get_rules.asciidoc b/docs/api/alerting/get_rules.asciidoc index 1594ec1fb7ae6..9c465b9f40ff8 100644 --- a/docs/api/alerting/get_rules.asciidoc +++ b/docs/api/alerting/get_rules.asciidoc @@ -1,5 +1,5 @@ [[get-rule-api]] -=== Get rule API +== Get rule API ++++ Get rule ++++ @@ -7,35 +7,46 @@ Retrieve a rule by ID. [[get-rule-api-request]] -==== Request +=== {api-request-title} `GET :/api/alerting/rule/` `GET :/s//api/alerting/rule/` +=== {api-prereq-title} + +You must have `read` privileges for the appropriate {kib} features, depending on +the `consumer` and `rule_type_id` of the rules you're seeking. For example, the +*Management* > *Stack Rules* feature, *Analytics* > *Discover* and *{ml-app}* +features, *{observability}*, and *Security* features. To get rules associated +with the *{stack-monitor-app}*, use the `monitoring_user` built-in role. + +For more details, refer to <>. + [[get-rule-api-params]] -==== Path parameters +=== {api-path-parms-title} `id`:: - (Required, string) The ID of the rule to retrieve. +(Required, string) The identifier of the rule to retrieve. `space_id`:: - (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. +(Optional, string) An identifier for the space. If `space_id` is not provided in +the URL, the default space is used. [[get-rule-api-codes]] -==== Response code +=== {api-response-codes-title} `200`:: - Indicates a successful call. +Indicates a successful call. [[get-rule-api-example]] -==== Example +=== {api-examples-title} Retrieve the rule object with the ID `41893910-6bca-11eb-9e0d-85d233e3ee35`: [source,sh] -------------------------------------------------- -$ curl -X GET api/alerting/rule/41893910-6bca-11eb-9e0d-85d233e3ee35 +GET api/alerting/rule/41893910-6bca-11eb-9e0d-85d233e3ee35 -------------------------------------------------- // KIBANA @@ -69,7 +80,8 @@ The API returns the following: "scheduled_task_id": "0b092d90-6b62-11eb-9e0d-85d233e3ee35", "execution_status": { "last_execution_date": "2021-02-10T17:55:14.262Z", - "status": "ok" + "status": "ok", + "last_duration": 359 } } -------------------------------------------------- diff --git a/docs/api/alerting/health.asciidoc b/docs/api/alerting/health.asciidoc index 24955bb81fa98..1f0c8936419b5 100644 --- a/docs/api/alerting/health.asciidoc +++ b/docs/api/alerting/health.asciidoc @@ -1,38 +1,45 @@ [[get-alerting-framework-health-api]] -=== Get Alerting framework health API +== Get alerting framework health API ++++ -Get Alerting framework health +Get alerting framework health ++++ -Retrieve the health status of the Alerting framework. +Retrieve the health status of the alerting framework. [[get-alerting-framework-health-api-request]] -==== Request +=== {api-request-title} `GET :/api/alerting/_health` `GET :/s//api/alerting/_health` +=== {api-prereq-title} + +You must have `read` privileges for the *Management* > *Stack Rules* feature or +for at least one of the *Analytics* > *Discover*, *Analytics* > *{ml-app}*, +*{observability}*, or *Security* features. + [[get-alerting-framework-health-api-params]] -==== Path parameters +=== {api-path-parms-title} `space_id`:: - (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. +(Optional, string) An identifier for the space. If `space_id` is not provided in +the URL, the default space is used. [[get-alerting-framework-health-api-codes]] -==== Response code +=== {api-response-codes-title} `200`:: - Indicates a successful call. +Indicates a successful call. [[get-alerting-framework-health-api-example]] -==== Example +=== {api-examples-title} -Retrieve the health status of the Alerting framework: +Retrieve the health status of the alerting framework: [source,sh] -------------------------------------------------- -$ curl -X GET api/alerting/_health +GET api/alerting/_health -------------------------------------------------- // KIBANA @@ -41,56 +48,46 @@ The API returns the following: [source,sh] -------------------------------------------------- { - "is_sufficiently_secure":true, - "has_permanent_encryption_key":true, - "alerting_framework_health":{ + "is_sufficiently_secure":true, <1> + "has_permanent_encryption_key":true, <2> + "alerting_framework_health":{ <3> "decryption_health":{ "status":"ok", - "timestamp":"2021-02-10T23:35:04.949Z" + "timestamp":"2022-06-21T21:46:15.023Z" + }, + "execution_health":{ + "status":"ok", + "timestamp":"2022-06-21T21:46:15.023Z" + }, + "read_health":{ + "status":"ok", + "timestamp":"2022-06-21T21:46:15.023Z" + } + }, + "alerting_framework_heath":{ <4> + "_deprecated":"This state property has a typo, use \"alerting_framework_health\" instead.","decryption_health":{ + "status":"ok", + "timestamp":"2022-06-21T21:46:15.023Z" }, "execution_health":{ "status":"ok", - "timestamp":"2021-02-10T23:35:04.949Z" + "timestamp":"2022-06-21T21:46:15.023Z" }, "read_health":{ "status":"ok", - "timestamp":"2021-02-10T23:35:04.949Z" + "timestamp":"2022-06-21T21:46:15.023Z" } } } -------------------------------------------------- - -The health API response contains the following properties: - -[cols="2*<"] -|=== - -| `is_sufficiently_secure` -| Returns `false` if security is enabled, but TLS is not. - -| `has_permanent_encryption_key` -| Return the state `false` if Encrypted Saved Object plugin has not a permanent encryption Key. - -| `alerting_framework_health` -| This state property has three substates that identify the health of the alerting framework API: `decryption_health`, `execution_health`, and `read_health`. - -| deprecated::`alerting_framework_heath` -| This state property has a typo, use `alerting_framework_health` instead. It has three substates that identify the health of the alerting framework API: `decryption_health`, `execution_health`, and `read_health`. - -|=== - -`alerting_framework_health` consists of the following properties: - -[cols="2*<"] -|=== - -| `decryption_health` -| Returns the timestamp and status of the rule decryption: `ok`, `warn` or `error` . - -| `execution_health` -| Returns the timestamp and status of the rule execution: `ok`, `warn` or `error`. - -| `read_health` -| Returns the timestamp and status of the rule reading events: `ok`, `warn` or `error`. - -|=== +<1> `is_sufficiently_secure` is `false` when security is enabled, but TLS is not. +<2> `has_permanent_encryption_key` is `false` when the encrypted saved object +plugin does not have a permanent encryption key. +<3> `alerting_framework_health` has three substates that identify the health of +the alerting framework: `decryption_health`, `execution_health`, and +`read_health`. `decryption_health` returns the timestamp and status of the rule +decryption: `ok`, `warn` or `error`. `execution_health` returns the timestamp +and status of the rule execution: `ok`, `warn` or `error`. `read_health` returns +the timestamp and status of the rule reading events: `ok`, `warn` or `error`. +<4> `alerting_framework_heath` has a typo, use `alerting_framework_health` +instead. deprecated:[8.0.0] diff --git a/docs/api/alerting/update_rule.asciidoc b/docs/api/alerting/update_rule.asciidoc index ecce62912939d..19a9c7c0144b0 100644 --- a/docs/api/alerting/update_rule.asciidoc +++ b/docs/api/alerting/update_rule.asciidoc @@ -15,9 +15,10 @@ Update the attributes for an existing rule. === {api-prereq-title} -You must have `all` privileges for the *Management* > *Stack Rules* feature or -for the *{ml-app}*, *{observability}*, or *Security* features, depending on the -`consumer` and `rule_type_id` of the rule you're updating. If the rule has +You must have `all` privileges for the appropriate {kib} features, depending on +the `consumer` and `rule_type_id` of the rule you're updating. For example, the +*Management* > *Stack Rules* feature, *Analytics* > *Discover* and *{ml-app}* +features, *{observability}*, or *Security* features. If the rule has `actions`, you must also have `read` privileges for the *Management* > *Actions and Connectors* feature. For more details, refer to <>. From b41bc6643dbcdbe47a155bfd846cdefbe7d33cf5 Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Wed, 22 Jun 2022 18:19:29 -0400 Subject: [PATCH 04/54] [Security Solution][Investigations] - Disable adding notes when read only (#133905) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../body/actions/add_note_icon_item.test.tsx | 68 +++++++++++++++++++ .../body/actions/add_note_icon_item.tsx | 34 ++++++---- .../body/actions/pin_event_action.test.tsx | 68 +++++++++++++++++++ .../body/actions/pin_event_action.tsx | 3 + .../body/events/event_column_view.test.tsx | 11 +++ .../components/timeline/body/index.test.tsx | 15 +++- .../timeline/notes_tab_content/index.tsx | 4 +- .../components/timeline/pin/index.tsx | 5 +- .../timeline/properties/helpers.tsx | 9 ++- 9 files changed, 196 insertions(+), 21 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.test.tsx diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.test.tsx new file mode 100644 index 0000000000000..1f4d0e9e8d12c --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.test.tsx @@ -0,0 +1,68 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { AddEventNoteAction } from './add_note_icon_item'; +import { useUserPrivileges } from '../../../../../common/components/user_privileges'; +import { getEndpointPrivilegesInitialStateMock } from '../../../../../common/components/user_privileges/endpoint/mocks'; +import { TestProviders } from '../../../../../common/mock'; +import { TimelineType } from '../../../../../../common/types'; + +jest.mock('../../../../../common/components/user_privileges'); +const useUserPrivilegesMock = useUserPrivileges as jest.Mock; + +describe('AddEventNoteAction', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('isDisabled', () => { + test('it disables the add note button when the user does NOT have crud privileges', () => { + useUserPrivilegesMock.mockReturnValue({ + kibanaSecuritySolutionsPrivileges: { crud: false, read: true }, + endpointPrivileges: getEndpointPrivilegesInitialStateMock(), + }); + + render( + + + + ); + + expect(screen.getByTestId('timeline-notes-button-small')).toHaveClass( + 'euiButtonIcon-isDisabled' + ); + }); + + test('it enables the add note button when the user has crud privileges', () => { + useUserPrivilegesMock.mockReturnValue({ + kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + endpointPrivileges: getEndpointPrivilegesInitialStateMock(), + }); + + render( + + + + ); + + expect(screen.getByTestId('timeline-notes-button-small')).not.toHaveClass( + 'euiButtonIcon-isDisabled' + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx index 22c40ba78b0fe..784685997e5ff 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx @@ -11,6 +11,7 @@ import { TimelineType } from '../../../../../../common/types/timeline'; import * as i18n from '../translations'; import { NotesButton } from '../../properties/helpers'; import { ActionIconItem } from './action_icon_item'; +import { useUserPrivileges } from '../../../../../common/components/user_privileges'; interface AddEventNoteActionProps { ariaLabel?: string; @@ -24,20 +25,25 @@ const AddEventNoteActionComponent: React.FC = ({ showNotes, timelineType, toggleShowNotes, -}) => ( - - - -); +}) => { + const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges(); + + return ( + + + + ); +}; AddEventNoteActionComponent.displayName = 'AddEventNoteActionComponent'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.test.tsx new file mode 100644 index 0000000000000..e94c2ac2a38f0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.test.tsx @@ -0,0 +1,68 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { PinEventAction } from './pin_event_action'; +import { useUserPrivileges } from '../../../../../common/components/user_privileges'; +import { getEndpointPrivilegesInitialStateMock } from '../../../../../common/components/user_privileges/endpoint/mocks'; +import { TestProviders } from '../../../../../common/mock'; +import { TimelineType } from '../../../../../../common/types'; + +jest.mock('../../../../../common/components/user_privileges'); +const useUserPrivilegesMock = useUserPrivileges as jest.Mock; + +describe('PinEventAction', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('isDisabled', () => { + test('it disables the pin event button when the user does NOT have crud privileges', () => { + useUserPrivilegesMock.mockReturnValue({ + kibanaSecuritySolutionsPrivileges: { crud: false, read: true }, + endpointPrivileges: getEndpointPrivilegesInitialStateMock(), + }); + + render( + + + + ); + + expect(screen.getByTestId('pin')).toHaveClass('euiButtonIcon-isDisabled'); + }); + + test('it enables the pin event button when the user has crud privileges', () => { + useUserPrivilegesMock.mockReturnValue({ + kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + endpointPrivileges: getEndpointPrivilegesInitialStateMock(), + }); + + render( + + + + ); + + expect(screen.getByTestId('pin')).not.toHaveClass('euiButtonIcon-isDisabled'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx index 53970594c8c1c..d0294d3908590 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/pin_event_action.tsx @@ -13,6 +13,7 @@ import { EventsTdContent } from '../../styles'; import { eventHasNotes, getPinTooltip } from '../helpers'; import { Pin } from '../../pin'; import { TimelineType } from '../../../../../../common/types/timeline'; +import { useUserPrivileges } from '../../../../../common/components/user_privileges'; interface PinEventActionProps { ariaLabel?: string; @@ -31,6 +32,7 @@ const PinEventActionComponent: React.FC = ({ eventIsPinned, timelineType, }) => { + const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges(); const tooltipContent = useMemo( () => getPinTooltip({ @@ -50,6 +52,7 @@ const PinEventActionComponent: React.FC = ({ ariaLabel={ariaLabel} allowUnpinning={!eventHasNotes(noteIds)} data-test-subj="pin-event" + isDisabled={kibanaSecuritySolutionsPrivileges.crud === false} isAlert={isAlert} onClick={onPinClicked} pinned={eventIsPinned} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx index 334bd464f700f..59b331d4d7f11 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx @@ -28,6 +28,17 @@ jest.mock('../../../../../common/hooks/use_selector', () => ({ useShallowEqualSelector: jest.fn(), useDeepEqualSelector: jest.fn(), })); +jest.mock('../../../../../common/components/user_privileges', () => { + return { + useUserPrivileges: () => ({ + listPrivileges: { loading: false, error: undefined, result: undefined }, + detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, + endpointPrivileges: {}, + kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + }), + }; +}); + jest.mock('../../../../../common/lib/kibana', () => ({ useKibana: () => ({ services: { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 200c9810d9fe6..f2045327a42f7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -36,6 +36,17 @@ import { createStore, State } from '../../../../common/store'; jest.mock('../../../../common/lib/kibana/hooks'); jest.mock('../../../../common/hooks/use_app_toasts'); +jest.mock('../../../../common/components/user_privileges', () => { + return { + useUserPrivileges: () => ({ + listPrivileges: { loading: false, error: undefined, result: undefined }, + detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, + endpointPrivileges: {}, + kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, + }), + }; +}); + jest.mock('../../../../common/lib/kibana', () => { const originalModule = jest.requireActual('../../../../common/lib/kibana'); const mockCasesContract = jest.requireActual('@kbn/cases-plugin/public/mocks'); @@ -225,7 +236,7 @@ describe('Body', () => { mockDispatch.mockClear(); }); - test('Add a Note to an event', () => { + test('Add a note to an event', () => { const wrapper = mount( @@ -257,7 +268,7 @@ describe('Body', () => { ); }); - test('Add two Note to an event', () => { + test('Add two notes to an event', () => { const { storage } = createSecuritySolutionStorageMock(); const state: State = { ...mockGlobalState, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx index c2179abbb61df..7c25794a16c80 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx @@ -39,6 +39,7 @@ import { getTimelineNoteSelector } from './selectors'; import { DetailsPanel } from '../../side_panel'; import { getScrollToTopSelector } from '../tabs_content/selectors'; import { useScrollToTop } from '../../../../common/components/scroll_to_top'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; const FullWidthFlexGroup = styled(EuiFlexGroup)` width: 100%; @@ -131,6 +132,7 @@ interface NotesTabContentProps { const NotesTabContentComponent: React.FC = ({ timelineId }) => { const dispatch = useDispatch(); + const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges(); const getScrollToTop = useMemo(() => getScrollToTopSelector(), []); const scrollToTop = useShallowEqualSelector((state) => getScrollToTop(state, timelineId)); @@ -239,7 +241,7 @@ const NotesTabContentComponent: React.FC = ({ timelineId } showTimelineDescription /> - {!isImmutable && ( + {!isImmutable && kibanaSecuritySolutionsPrivileges.crud === true && ( void; pinned: boolean; @@ -45,7 +46,7 @@ export const getDefaultAriaLabel = ({ }; export const Pin = React.memo( - ({ ariaLabel, allowUnpinning, isAlert, onClick = noop, pinned, timelineType }) => { + ({ ariaLabel, allowUnpinning, isAlert, isDisabled, onClick = noop, pinned, timelineType }) => { const isTemplate = timelineType === TimelineType.template; const defaultAriaLabel = getDefaultAriaLabel({ isAlert, @@ -60,7 +61,7 @@ export const Pin = React.memo( data-test-subj="pin" iconType={getPinIcon(pinned)} onClick={onClick} - isDisabled={isTemplate || !allowUnpinning} + isDisabled={isDisabled || isTemplate || !allowUnpinning} size="s" /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index c59c0a15ff53d..ff0d8686bb9c3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -91,6 +91,7 @@ NewTimeline.displayName = 'NewTimeline'; interface NotesButtonProps { ariaLabel?: string; + isDisabled?: boolean; showNotes: boolean; toggleShowNotes: () => void; toolTip?: string; @@ -99,6 +100,7 @@ interface NotesButtonProps { interface SmallNotesButtonProps { ariaLabel?: string; + isDisabled?: boolean; toggleShowNotes: () => void; timelineType: TimelineTypeLiteral; } @@ -106,7 +108,7 @@ interface SmallNotesButtonProps { export const NOTES_BUTTON_CLASS_NAME = 'notes-button'; const SmallNotesButton = React.memo( - ({ ariaLabel = i18n.NOTES, toggleShowNotes, timelineType }) => { + ({ ariaLabel = i18n.NOTES, isDisabled, toggleShowNotes, timelineType }) => { const isTemplate = timelineType === TimelineType.template; return ( @@ -114,6 +116,7 @@ const SmallNotesButton = React.memo( aria-label={ariaLabel} className={NOTES_BUTTON_CLASS_NAME} data-test-subj="timeline-notes-button-small" + disabled={isDisabled} iconType="editorComment" onClick={toggleShowNotes} size="s" @@ -125,10 +128,11 @@ const SmallNotesButton = React.memo( SmallNotesButton.displayName = 'SmallNotesButton'; export const NotesButton = React.memo( - ({ ariaLabel, showNotes, timelineType, toggleShowNotes, toolTip }) => + ({ ariaLabel, isDisabled, showNotes, timelineType, toggleShowNotes, toolTip }) => showNotes ? ( @@ -136,6 +140,7 @@ export const NotesButton = React.memo( From e5d73a116908d82b875fc3ad248e57fa5c5f970c Mon Sep 17 00:00:00 2001 From: Baturalp Gurdin <9674241+suchcodemuchwow@users.noreply.github.com> Date: Thu, 23 Jun 2022 00:44:11 +0200 Subject: [PATCH 05/54] ingest performance metrics to ci-stats (#134792) --- .buildkite/pipelines/performance/daily.yml | 6 + .../functional/report_performance_metrics.sh | 21 +++ package.json | 5 + packages/BUILD.bazel | 2 + .../BUILD.bazel | 124 ++++++++++++++++++ .../README.md | 3 + .../jest.config.js | 13 ++ .../package.json | 7 + .../src/apm_client.ts | 98 ++++++++++++++ .../src/cli.ts | 81 ++++++++++++ .../src/index.ts | 10 ++ .../src/reporter.ts | 56 ++++++++ .../src/utils.ts | 19 +++ .../tsconfig.json | 17 +++ .../src/ci_stats_reporter.ts | 20 +++ packages/kbn-pm/dist/index.js | 23 ++++ scripts/report_performance_metrics.js | 10 ++ yarn.lock | 23 ++++ 18 files changed, 538 insertions(+) create mode 100644 .buildkite/scripts/steps/functional/report_performance_metrics.sh create mode 100644 packages/kbn-ci-stats-performance-metrics/BUILD.bazel create mode 100644 packages/kbn-ci-stats-performance-metrics/README.md create mode 100644 packages/kbn-ci-stats-performance-metrics/jest.config.js create mode 100644 packages/kbn-ci-stats-performance-metrics/package.json create mode 100644 packages/kbn-ci-stats-performance-metrics/src/apm_client.ts create mode 100644 packages/kbn-ci-stats-performance-metrics/src/cli.ts create mode 100644 packages/kbn-ci-stats-performance-metrics/src/index.ts create mode 100644 packages/kbn-ci-stats-performance-metrics/src/reporter.ts create mode 100644 packages/kbn-ci-stats-performance-metrics/src/utils.ts create mode 100644 packages/kbn-ci-stats-performance-metrics/tsconfig.json create mode 100644 scripts/report_performance_metrics.js diff --git a/.buildkite/pipelines/performance/daily.yml b/.buildkite/pipelines/performance/daily.yml index 4e243a23f1e02..fdc4ae17d69a2 100644 --- a/.buildkite/pipelines/performance/daily.yml +++ b/.buildkite/pipelines/performance/daily.yml @@ -25,6 +25,12 @@ steps: # queue: n2-2 # depends_on: tests + - label: ':chart_with_upwards_trend: Report performance metrics to ci-stats' + command: .buildkite/scripts/steps/functional/report_performance_metrics.sh + agents: + queue: n2-2 + depends_on: tests + - wait: ~ continue_on_failure: true diff --git a/.buildkite/scripts/steps/functional/report_performance_metrics.sh b/.buildkite/scripts/steps/functional/report_performance_metrics.sh new file mode 100644 index 0000000000000..66a5ac27a8dff --- /dev/null +++ b/.buildkite/scripts/steps/functional/report_performance_metrics.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/common/util.sh + +# TODO: Add new user and change lines accordingly +USER_FROM_VAULT="$(retry 5 5 vault read -field=username secret/kibana-issues/dev/ci_stats_performance_metrics)" +PASS_FROM_VAULT="$(retry 5 5 vault read -field=password secret/kibana-issues/dev/ci_stats_performance_metrics)" +APM_SERVER_URL="https://kibana-ops-e2e-perf.es.us-central1.gcp.cloud.es.io:9243/internal/apm" +BUILD_ID=${BUILDKITE_BUILD_ID} + +.buildkite/scripts/bootstrap.sh + +echo "--- Extract APM metrics & report them to ci-stats" + +node scripts/report_performance_metrics \ + --buildId "${BUILD_ID}" \ + --apm-url "${APM_SERVER_URL}" \ + --apm-username "${USER_FROM_VAULT}" \ + --apm-password "${PASS_FROM_VAULT}" diff --git a/package.json b/package.json index d0446690c06a3..14c0d5e1b0aa1 100644 --- a/package.json +++ b/package.json @@ -142,6 +142,7 @@ "@kbn/analytics-shippers-fullstory": "link:bazel-bin/packages/analytics/shippers/fullstory", "@kbn/apm-config-loader": "link:bazel-bin/packages/kbn-apm-config-loader", "@kbn/apm-utils": "link:bazel-bin/packages/kbn-apm-utils", + "@kbn/ci-stats-performance-metrics": "link:bazel-bin/packages/kbn-ci-stats-performance-metrics", "@kbn/coloring": "link:bazel-bin/packages/kbn-coloring", "@kbn/config": "link:bazel-bin/packages/kbn-config", "@kbn/config-mocks": "link:bazel-bin/packages/kbn-config-mocks", @@ -191,6 +192,7 @@ "@kbn/i18n-react": "link:bazel-bin/packages/kbn-i18n-react", "@kbn/interpreter": "link:bazel-bin/packages/kbn-interpreter", "@kbn/io-ts-utils": "link:bazel-bin/packages/kbn-io-ts-utils", + "@kbn/kbn-ci-stats-performance-metrics": "link:bazel-bin/packages/kbn-kbn-ci-stats-performance-metrics", "@kbn/kibana-json-schema": "link:bazel-bin/packages/kbn-kibana-json-schema", "@kbn/logging": "link:bazel-bin/packages/kbn-logging", "@kbn/logging-mocks": "link:bazel-bin/packages/kbn-logging-mocks", @@ -393,6 +395,7 @@ "proxy-from-env": "1.0.0", "puid": "1.0.7", "puppeteer": "^10.2.0", + "qs": "^6.10.5", "query-string": "^6.13.2", "random-word-slugs": "^0.0.5", "raw-loader": "^3.1.0", @@ -669,6 +672,7 @@ "@types/kbn__bazel-packages": "link:bazel-bin/packages/kbn-bazel-packages/npm_module_types", "@types/kbn__bazel-runner": "link:bazel-bin/packages/kbn-bazel-runner/npm_module_types", "@types/kbn__ci-stats-core": "link:bazel-bin/packages/kbn-ci-stats-core/npm_module_types", + "@types/kbn__ci-stats-performance-metrics": "link:bazel-bin/packages/kbn-ci-stats-performance-metrics/npm_module_types", "@types/kbn__ci-stats-reporter": "link:bazel-bin/packages/kbn-ci-stats-reporter/npm_module_types", "@types/kbn__cli-dev-mode": "link:bazel-bin/packages/kbn-cli-dev-mode/npm_module_types", "@types/kbn__coloring": "link:bazel-bin/packages/kbn-coloring/npm_module_types", @@ -734,6 +738,7 @@ "@types/kbn__interpreter": "link:bazel-bin/packages/kbn-interpreter/npm_module_types", "@types/kbn__io-ts-utils": "link:bazel-bin/packages/kbn-io-ts-utils/npm_module_types", "@types/kbn__jest-serializers": "link:bazel-bin/packages/kbn-jest-serializers/npm_module_types", + "@types/kbn__kbn-ci-stats-performance-metrics": "link:bazel-bin/packages/kbn-kbn-ci-stats-performance-metrics/npm_module_types", "@types/kbn__kibana-json-schema": "link:bazel-bin/packages/kbn-kibana-json-schema/npm_module_types", "@types/kbn__logging": "link:bazel-bin/packages/kbn-logging/npm_module_types", "@types/kbn__logging-mocks": "link:bazel-bin/packages/kbn-logging-mocks/npm_module_types", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index a0e952de7f874..2cce534bb98ef 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -62,6 +62,7 @@ filegroup( "//packages/kbn-bazel-packages:build", "//packages/kbn-bazel-runner:build", "//packages/kbn-ci-stats-core:build", + "//packages/kbn-ci-stats-performance-metrics:build", "//packages/kbn-ci-stats-reporter:build", "//packages/kbn-cli-dev-mode:build", "//packages/kbn-coloring:build", @@ -214,6 +215,7 @@ filegroup( "//packages/kbn-bazel-packages:build_types", "//packages/kbn-bazel-runner:build_types", "//packages/kbn-ci-stats-core:build_types", + "//packages/kbn-ci-stats-performance-metrics:build_types", "//packages/kbn-ci-stats-reporter:build_types", "//packages/kbn-cli-dev-mode:build_types", "//packages/kbn-coloring:build_types", diff --git a/packages/kbn-ci-stats-performance-metrics/BUILD.bazel b/packages/kbn-ci-stats-performance-metrics/BUILD.bazel new file mode 100644 index 0000000000000..ff475252a3e99 --- /dev/null +++ b/packages/kbn-ci-stats-performance-metrics/BUILD.bazel @@ -0,0 +1,124 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "kbn-ci-stats-performance-metrics" +PKG_REQUIRE_NAME = "@kbn/ci-stats-performance-metrics" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "//packages/kbn-dev-cli-errors", + "//packages/kbn-dev-cli-runner", + "//packages/kbn-test", + "//packages/kbn-tooling-log", + "//packages/kbn-ci-stats-reporter", +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "//packages/kbn-dev-cli-errors:npm_module_types", + "//packages/kbn-dev-cli-runner:npm_module_types", + "//packages/kbn-test:npm_module_types", + "//packages/kbn-tooling-log:npm_module_types", + "//packages/kbn-ci-stats-reporter:npm_module_types", + "@npm//@types/node", + "@npm//@types/jest", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-ci-stats-performance-metrics/README.md b/packages/kbn-ci-stats-performance-metrics/README.md new file mode 100644 index 0000000000000..8b1390e8dc7ab --- /dev/null +++ b/packages/kbn-ci-stats-performance-metrics/README.md @@ -0,0 +1,3 @@ +# @kbn/ci-stats-performance-metrics + +Empty package generated by @kbn/generate diff --git a/packages/kbn-ci-stats-performance-metrics/jest.config.js b/packages/kbn-ci-stats-performance-metrics/jest.config.js new file mode 100644 index 0000000000000..98fc60a9a052a --- /dev/null +++ b/packages/kbn-ci-stats-performance-metrics/jest.config.js @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../..', + roots: ['/packages/kbn-ci-stats-performance-metrics'], +}; diff --git a/packages/kbn-ci-stats-performance-metrics/package.json b/packages/kbn-ci-stats-performance-metrics/package.json new file mode 100644 index 0000000000000..0801174ab4a02 --- /dev/null +++ b/packages/kbn-ci-stats-performance-metrics/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/ci-stats-performance-metrics", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/kbn-ci-stats-performance-metrics/src/apm_client.ts b/packages/kbn-ci-stats-performance-metrics/src/apm_client.ts new file mode 100644 index 0000000000000..ac72b79dc5fb4 --- /dev/null +++ b/packages/kbn-ci-stats-performance-metrics/src/apm_client.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; +import { ToolingLog } from '@kbn/tooling-log'; +import { getYearAgoIso } from './utils'; + +type Environment = 'ENVIRONMENT_ALL' | 'ci' | 'development'; +type LatencyAggregationType = 'avg' | 'p95' | 'p99'; +type TransactionType = 'page-load' | 'app-change' | 'user-interaction' | 'http-request'; + +interface MainStatisticsRequestOptions { + ciBuildId: string; + start?: string; + end?: string; + environment?: Environment; + transactionType?: TransactionType; + latencyAggregationType?: LatencyAggregationType; +} + +export interface TransactionGroup { + name: string; + latency: number; + throughput: number; + errorRate?: any; + impact: number; + transactionType: TransactionType; +} + +export interface MainStatisticsResponse { + transactionGroups: TransactionGroup[]; + isAggregationAccurate: boolean; + bucketSize: number; +} + +const DEFAULT_BASE_URL = + 'https://kibana-ops-e2e-perf.kb.us-central1.gcp.cloud.es.io:9243/internal/apm'; +const DEFAULT_CLIENT_TIMEOUT = 120 * 1000; + +export class ApmClient { + private readonly client: AxiosInstance; + private readonly logger: ToolingLog; + + constructor(config: AxiosRequestConfig, logger: ToolingLog) { + const { baseURL = DEFAULT_BASE_URL, timeout = DEFAULT_CLIENT_TIMEOUT, auth } = config; + + this.client = axios.create({ + auth, + baseURL, + timeout, + }); + + this.logger = logger || console; + } + + public get baseUrl(): string | undefined { + return this.client.defaults.baseURL; + } + + public async mainStatistics(queryParams: MainStatisticsRequestOptions) { + const { now, yearAgo } = getYearAgoIso(); + + const { + ciBuildId, + start = yearAgo, + end = now, + environment = 'ENVIRONMENT_ALL', + transactionType = 'page-load', + latencyAggregationType = 'avg', + } = queryParams; + + try { + const responseRaw = await this.client.get( + `services/kibana-frontend/transactions/groups/main_statistics`, + { + params: { + kuery: `labels.ciBuildId:${ciBuildId}`, + environment, + start, + end, + transactionType, + latencyAggregationType, + }, + } + ); + return responseRaw.data; + } catch (error) { + this.logger.error( + `Error fetching main statistics from APM, ci build ${ciBuildId}, error message ${error.message}` + ); + } + } +} diff --git a/packages/kbn-ci-stats-performance-metrics/src/cli.ts b/packages/kbn-ci-stats-performance-metrics/src/cli.ts new file mode 100644 index 0000000000000..49af12a48bb19 --- /dev/null +++ b/packages/kbn-ci-stats-performance-metrics/src/cli.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** *********************************************************** + * + * Run `node scripts/extract_performance_testing_dataset --help` for usage information + * + *************************************************************/ + +import { run } from '@kbn/dev-cli-runner'; +import { createFlagError } from '@kbn/dev-cli-errors'; +import { reporter } from './reporter'; + +export async function runCli() { + run( + async ({ log, flags }) => { + const apmBaseUrl = flags['apm-url']; + if (apmBaseUrl && typeof apmBaseUrl !== 'string') { + throw createFlagError('--apm-url must be a string'); + } + if (!apmBaseUrl) { + throw createFlagError('--apm-url must be defined'); + } + + const apmUsername = flags['apm-username']; + if (apmUsername && typeof apmUsername !== 'string') { + throw createFlagError('--apm-username must be a string'); + } + if (!apmUsername) { + throw createFlagError('--apm-username must be defined'); + } + + const apmPassword = flags['apm-password']; + if (apmPassword && typeof apmPassword !== 'string') { + throw createFlagError('--apm-password must be a string'); + } + if (!apmPassword) { + throw createFlagError('--apm-password must be defined'); + } + + const buildId = flags.buildId; + if (buildId && typeof buildId !== 'string') { + throw createFlagError('--buildId must be a string'); + } + if (!buildId) { + throw createFlagError('--buildId must be defined'); + } + + return reporter({ + apmClient: { + auth: { + username: apmUsername, + password: apmPassword, + }, + baseURL: apmBaseUrl, + }, + param: { + ciBuildId: buildId, + }, + log, + }); + }, + { + description: `CLI to fetch performance metrics and report those to ci-stats`, + flags: { + string: ['buildId', 'apm-url', 'apm-username', 'apm-password'], + help: ` + --buildId BUILDKITE_JOB_ID or uuid generated locally, stored in APM-based document as label: 'labels.testBuildId' + --apm-url url for APM cluster + --apm-username username for Apm + --apm-password password for Apm + `, + }, + } + ); +} diff --git a/packages/kbn-ci-stats-performance-metrics/src/index.ts b/packages/kbn-ci-stats-performance-metrics/src/index.ts new file mode 100644 index 0000000000000..0da7aee1a6ef2 --- /dev/null +++ b/packages/kbn-ci-stats-performance-metrics/src/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { reporter } from './reporter'; +export { runCli } from './cli'; diff --git a/packages/kbn-ci-stats-performance-metrics/src/reporter.ts b/packages/kbn-ci-stats-performance-metrics/src/reporter.ts new file mode 100644 index 0000000000000..904eff2393e96 --- /dev/null +++ b/packages/kbn-ci-stats-performance-metrics/src/reporter.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ToolingLog } from '@kbn/tooling-log'; +import { CiStatsReporter } from '@kbn/ci-stats-reporter'; + +import { ApmClient } from './apm_client'; + +interface ReporterOptions { + param: { + ciBuildId: string; + }; + apmClient: { + baseURL: string; + auth: { + username: string; + password: string; + }; + }; + log: ToolingLog; +} + +export async function reporter(options: ReporterOptions) { + const { + param: { ciBuildId }, + apmClient: apmClientOptions, + log, + } = options; + + const apm = new ApmClient(apmClientOptions, log); + + const performanceMainStats = await apm.mainStatistics({ ciBuildId }); + + if (performanceMainStats) { + const { transactionGroups: tg } = performanceMainStats; + + const loginStats = tg.find((e) => e.name === '/login'); + const appHomeStats = tg.find((e) => e.name === '/app/home'); + const appDashboardsStats = tg.find((e) => e.name === '/app/dashboards'); + + const ciStatsReporter = CiStatsReporter.fromEnv(log); + + const body = { + ...(loginStats && { page_load_login: loginStats.latency }), + ...(appHomeStats && { page_load_app_home: appHomeStats.latency }), + ...(appDashboardsStats && { page_load_app_dashboards: appDashboardsStats.latency }), + }; + + await ciStatsReporter.reportPerformanceMetrics(body); + } +} diff --git a/packages/kbn-ci-stats-performance-metrics/src/utils.ts b/packages/kbn-ci-stats-performance-metrics/src/utils.ts new file mode 100644 index 0000000000000..bd22c0569b247 --- /dev/null +++ b/packages/kbn-ci-stats-performance-metrics/src/utils.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export function getYearAgoIso() { + const d = new Date(); + const nowIso = d.toISOString(); + d.setMonth(d.getMonth() - 12); + const yearAgoIso = d.toISOString(); + + return { + now: nowIso, + yearAgo: yearAgoIso, + }; +} diff --git a/packages/kbn-ci-stats-performance-metrics/tsconfig.json b/packages/kbn-ci-stats-performance-metrics/tsconfig.json new file mode 100644 index 0000000000000..a8cfc2cceb08b --- /dev/null +++ b/packages/kbn-ci-stats-performance-metrics/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-ci-stats-reporter/src/ci_stats_reporter.ts b/packages/kbn-ci-stats-reporter/src/ci_stats_reporter.ts index 5d3f17a76f687..7a23f20143d94 100644 --- a/packages/kbn-ci-stats-reporter/src/ci_stats_reporter.ts +++ b/packages/kbn-ci-stats-reporter/src/ci_stats_reporter.ts @@ -57,6 +57,8 @@ export interface CiStatsMetric { meta?: CiStatsMetadata; } +export type PerformanceMetrics = Record; + /** A ci-stats timing event */ export interface CiStatsTiming { /** Top-level categorization for the timing, e.g. "scripts/foo", process type, etc. */ @@ -306,6 +308,24 @@ export class CiStatsReporter { } } + async reportPerformanceMetrics(metrics: PerformanceMetrics) { + if (!this.hasBuildConfig()) { + return; + } + + const buildId = this.config?.buildId; + if (!buildId) { + throw new Error(`Performance metrics can't be reported without a buildId`); + } + + return !!(await this.req({ + auth: true, + path: `/v1/performance_metrics_report?buildId=${buildId}`, + body: { metrics }, + bodyDesc: `performance metrics: ${metrics}`, + })); + } + /** * In order to allow this code to run before @kbn/utils is built, @kbn/pm will pass * in the upstreamBranch when calling the timings() method. Outside of @kbn/pm diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 329bd18e33952..17bafb19a996c 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -1806,6 +1806,29 @@ class CiStatsReporter { await flushBuffer(); } } + + async reportPerformanceMetrics(metrics) { + var _this$config9; + + if (!this.hasBuildConfig()) { + return; + } + + const buildId = (_this$config9 = this.config) === null || _this$config9 === void 0 ? void 0 : _this$config9.buildId; + + if (!buildId) { + throw new Error(`Performance metrics can't be reported without a buildId`); + } + + return !!(await this.req({ + auth: true, + path: `/v1/performance_metrics_report?buildId=${buildId}`, + body: { + metrics + }, + bodyDesc: `performance metrics: ${metrics}` + })); + } /** * In order to allow this code to run before @kbn/utils is built, @kbn/pm will pass * in the upstreamBranch when calling the timings() method. Outside of @kbn/pm diff --git a/scripts/report_performance_metrics.js b/scripts/report_performance_metrics.js new file mode 100644 index 0000000000000..8fc7e5c4b52a3 --- /dev/null +++ b/scripts/report_performance_metrics.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +require('../src/setup_node_env'); +require('@kbn/ci-stats-performance-metrics').runCli(); diff --git a/yarn.lock b/yarn.lock index 96f12f915c47e..2c9dcd462951b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2991,6 +2991,10 @@ version "0.0.0" uid "" +"@kbn/ci-stats-performance-metrics@link:bazel-bin/packages/kbn-ci-stats-performance-metrics": + version "0.0.0" + uid "" + "@kbn/ci-stats-reporter@link:bazel-bin/packages/kbn-ci-stats-reporter": version "0.0.0" uid "" @@ -3251,6 +3255,10 @@ version "0.0.0" uid "" +"@kbn/kbn-ci-stats-performance-metrics@link:bazel-bin/packages/kbn-kbn-ci-stats-performance-metrics": + version "0.0.0" + uid "" + "@kbn/kibana-json-schema@link:bazel-bin/packages/kbn-kibana-json-schema": version "0.0.0" uid "" @@ -6402,6 +6410,10 @@ version "0.0.0" uid "" +"@types/kbn__ci-stats-performance-metrics@link:bazel-bin/packages/kbn-ci-stats-performance-metrics/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__ci-stats-reporter@link:bazel-bin/packages/kbn-ci-stats-reporter/npm_module_types": version "0.0.0" uid "" @@ -6662,6 +6674,10 @@ version "0.0.0" uid "" +"@types/kbn__kbn-ci-stats-performance-metrics@link:bazel-bin/packages/kbn-kbn-ci-stats-performance-metrics/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__kibana-json-schema@link:bazel-bin/packages/kbn-kibana-json-schema/npm_module_types": version "0.0.0" uid "" @@ -23863,6 +23879,13 @@ qs@^6.10.0: dependencies: side-channel "^1.0.4" +qs@^6.10.5: + version "6.10.5" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.5.tgz#974715920a80ff6a262264acd2c7e6c2a53282b4" + integrity sha512-O5RlPh0VFtR78y79rgcgKK4wbAI0C5zGVLztOIdpWX6ep368q5Hv6XRxDvXuZ9q3C6v+e3n8UfZZJw7IIG27eQ== + dependencies: + side-channel "^1.0.4" + qs@^6.7.0: version "6.9.4" resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" From 9a1e1d00a43ff825892457463363551b3ecc05f5 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 23 Jun 2022 00:22:00 +0100 Subject: [PATCH 06/54] chore(NA): auto bootstrap after removing node modules manually (#134961) * chore(NA): mechanism for autobootstrap when manually removing node_modules * fix(NA): check folders with isDirectory * fix(NA): check folders with isDirectory * fix(NA): check folders with isDirectory * docs(NA): update typo on code comment --- packages/kbn-pm/dist/index.js | 50 ++++++++++++----- packages/kbn-pm/src/commands/bootstrap.ts | 14 +++-- packages/kbn-pm/src/utils/bazel/index.ts | 2 +- packages/kbn-pm/src/utils/bazel/yarn.ts | 53 +++++++++++++++++++ .../kbn-pm/src/utils/bazel/yarn_integrity.ts | 24 --------- 5 files changed, 103 insertions(+), 40 deletions(-) create mode 100644 packages/kbn-pm/src/utils/bazel/yarn.ts delete mode 100644 packages/kbn-pm/src/utils/bazel/yarn_integrity.ts diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 17bafb19a996c..3df97ff607ad8 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -61971,12 +61971,14 @@ const BootstrapCommand = { ms: Date.now() - start }); } - }; // Force install is set in case a flag is passed into yarn kbn bootstrap + }; // Force install is set in case a flag is passed into yarn kbn bootstrap or + // our custom logic have determined there is a chance node_modules have been manually deleted and as such bazel + // tracking mechanism is no longer valid - const forceInstall = !!options && options['force-install'] === true; // Install bazel machinery tools if needed + const forceInstall = !!options && options['force-install'] === true || (await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_8__[/* haveNodeModulesBeenManuallyDeleted */ "c"])(kibanaProjectPath)); // Install bazel machinery tools if needed - await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_8__[/* installBazelTools */ "c"])(rootPath); // Setup remote cache settings in .bazelrc.cache if needed + await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_8__[/* installBazelTools */ "d"])(rootPath); // Setup remote cache settings in .bazelrc.cache if needed await Object(_utils_bazel_setup_remote_cache__WEBPACK_IMPORTED_MODULE_9__[/* setupRemoteCache */ "a"])(rootPath); // Bootstrap process for Bazel packages // Bazel is now managing dependencies so yarn install @@ -61990,7 +61992,7 @@ const BootstrapCommand = { if (forceInstall) { await time('force install dependencies', async () => { - await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_8__[/* removeYarnIntegrityFileIfExists */ "e"])(path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(kibanaProjectPath, 'node_modules')); + await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_8__[/* removeYarnIntegrityFileIfExists */ "f"])(path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(kibanaProjectPath, 'node_modules')); await Object(_kbn_bazel_runner__WEBPACK_IMPORTED_MODULE_2__["runBazel"])({ bazelArgs: ['clean', '--expunge'], log: _utils_log__WEBPACK_IMPORTED_MODULE_3__[/* log */ "a"] @@ -62171,7 +62173,7 @@ const CleanCommand = { } // Runs Bazel soft clean - if (await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_5__[/* isBazelBinAvailable */ "d"])(kbn.getAbsolute())) { + if (await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_5__[/* isBazelBinAvailable */ "e"])(kbn.getAbsolute())) { await Object(_kbn_bazel_runner__WEBPACK_IMPORTED_MODULE_4__["runBazel"])({ bazelArgs: ['clean'], log: _utils_log__WEBPACK_IMPORTED_MODULE_7__[/* log */ "a"] @@ -62331,7 +62333,7 @@ const ResetCommand = { } // Runs Bazel hard clean and deletes Bazel Cache Folders - if (await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_5__[/* isBazelBinAvailable */ "d"])(kbn.getAbsolute())) { + if (await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_5__[/* isBazelBinAvailable */ "e"])(kbn.getAbsolute())) { // Hard cleaning bazel await Object(_kbn_bazel_runner__WEBPACK_IMPORTED_MODULE_4__["runBazel"])({ bazelArgs: ['clean', '--expunge'], @@ -62799,12 +62801,14 @@ async function getBazelRepositoryCacheFolder() { /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "b", function() { return _get_cache_folders__WEBPACK_IMPORTED_MODULE_0__["b"]; }); /* harmony import */ var _install_tools__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./src/utils/bazel/install_tools.ts"); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "c", function() { return _install_tools__WEBPACK_IMPORTED_MODULE_1__["a"]; }); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "d", function() { return _install_tools__WEBPACK_IMPORTED_MODULE_1__["a"]; }); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "d", function() { return _install_tools__WEBPACK_IMPORTED_MODULE_1__["b"]; }); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "e", function() { return _install_tools__WEBPACK_IMPORTED_MODULE_1__["b"]; }); -/* harmony import */ var _yarn_integrity__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__("./src/utils/bazel/yarn_integrity.ts"); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "e", function() { return _yarn_integrity__WEBPACK_IMPORTED_MODULE_2__["a"]; }); +/* harmony import */ var _yarn__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__("./src/utils/bazel/yarn.ts"); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "c", function() { return _yarn__WEBPACK_IMPORTED_MODULE_2__["a"]; }); + +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "f", function() { return _yarn__WEBPACK_IMPORTED_MODULE_2__["b"]; }); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one @@ -63007,11 +63011,12 @@ async function setupRemoteCache(repoRootPath) { /***/ }), -/***/ "./src/utils/bazel/yarn_integrity.ts": +/***/ "./src/utils/bazel/yarn.ts": /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return removeYarnIntegrityFileIfExists; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "b", function() { return removeYarnIntegrityFileIfExists; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return haveNodeModulesBeenManuallyDeleted; }); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("path"); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var _fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./src/utils/fs.ts"); @@ -63023,6 +63028,7 @@ async function setupRemoteCache(repoRootPath) { * Side Public License, v 1. */ + // yarn integrity file checker async function removeYarnIntegrityFileIfExists(nodeModulesPath) { try { @@ -63034,6 +63040,26 @@ async function removeYarnIntegrityFileIfExists(nodeModulesPath) { } } catch {// no-op } +} // yarn and bazel integration checkers + +async function areNodeModulesPresent(kbnRootPath) { + try { + return await Object(_fs__WEBPACK_IMPORTED_MODULE_1__[/* isDirectory */ "c"])(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(kbnRootPath, 'node_modules')); + } catch { + return false; + } +} + +async function haveBazelFoldersBeenCreatedBefore(kbnRootPath) { + try { + return (await Object(_fs__WEBPACK_IMPORTED_MODULE_1__[/* isDirectory */ "c"])(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(kbnRootPath, 'bazel-bin', 'packages'))) || (await Object(_fs__WEBPACK_IMPORTED_MODULE_1__[/* isDirectory */ "c"])(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(kbnRootPath, 'bazel-kibana', 'packages'))) || (await Object(_fs__WEBPACK_IMPORTED_MODULE_1__[/* isDirectory */ "c"])(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(kbnRootPath, 'bazel-out', 'host'))); + } catch { + return false; + } +} + +async function haveNodeModulesBeenManuallyDeleted(kbnRootPath) { + return !(await areNodeModulesPresent(kbnRootPath)) && (await haveBazelFoldersBeenCreatedBefore(kbnRootPath)); } /***/ }), diff --git a/packages/kbn-pm/src/commands/bootstrap.ts b/packages/kbn-pm/src/commands/bootstrap.ts index 3f9275cff8e61..8ac55b3478363 100644 --- a/packages/kbn-pm/src/commands/bootstrap.ts +++ b/packages/kbn-pm/src/commands/bootstrap.ts @@ -16,7 +16,11 @@ import { linkProjectExecutables } from '../utils/link_project_executables'; import { ICommand } from '.'; import { readYarnLock } from '../utils/yarn_lock'; import { validateDependencies } from '../utils/validate_dependencies'; -import { installBazelTools, removeYarnIntegrityFileIfExists } from '../utils/bazel'; +import { + installBazelTools, + haveNodeModulesBeenManuallyDeleted, + removeYarnIntegrityFileIfExists, +} from '../utils/bazel'; import { setupRemoteCache } from '../utils/bazel/setup_remote_cache'; export const BootstrapCommand: ICommand = { @@ -46,8 +50,12 @@ export const BootstrapCommand: ICommand = { } }; - // Force install is set in case a flag is passed into yarn kbn bootstrap - const forceInstall = !!options && options['force-install'] === true; + // Force install is set in case a flag is passed into yarn kbn bootstrap or + // our custom logic have determined there is a chance node_modules have been manually deleted and as such bazel + // tracking mechanism is no longer valid + const forceInstall = + (!!options && options['force-install'] === true) || + (await haveNodeModulesBeenManuallyDeleted(kibanaProjectPath)); // Install bazel machinery tools if needed await installBazelTools(rootPath); diff --git a/packages/kbn-pm/src/utils/bazel/index.ts b/packages/kbn-pm/src/utils/bazel/index.ts index 39b3cb9c61f00..d1460d5598f55 100644 --- a/packages/kbn-pm/src/utils/bazel/index.ts +++ b/packages/kbn-pm/src/utils/bazel/index.ts @@ -8,4 +8,4 @@ export * from './get_cache_folders'; export * from './install_tools'; -export * from './yarn_integrity'; +export * from './yarn'; diff --git a/packages/kbn-pm/src/utils/bazel/yarn.ts b/packages/kbn-pm/src/utils/bazel/yarn.ts new file mode 100644 index 0000000000000..24e44be3b3cdf --- /dev/null +++ b/packages/kbn-pm/src/utils/bazel/yarn.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { join, resolve } from 'path'; +import { isDirectory, isFile, tryRealpath, unlink } from '../fs'; + +// yarn integrity file checker +export async function removeYarnIntegrityFileIfExists(nodeModulesPath: string) { + try { + const nodeModulesRealPath = await tryRealpath(nodeModulesPath); + const yarnIntegrityFilePath = join(nodeModulesRealPath, '.yarn-integrity'); + + // check if the file exists and delete it in that case + if (await isFile(yarnIntegrityFilePath)) { + await unlink(yarnIntegrityFilePath); + } + } catch { + // no-op + } +} + +// yarn and bazel integration checkers +async function areNodeModulesPresent(kbnRootPath: string) { + try { + return await isDirectory(resolve(kbnRootPath, 'node_modules')); + } catch { + return false; + } +} + +async function haveBazelFoldersBeenCreatedBefore(kbnRootPath: string) { + try { + return ( + (await isDirectory(resolve(kbnRootPath, 'bazel-bin', 'packages'))) || + (await isDirectory(resolve(kbnRootPath, 'bazel-kibana', 'packages'))) || + (await isDirectory(resolve(kbnRootPath, 'bazel-out', 'host'))) + ); + } catch { + return false; + } +} + +export async function haveNodeModulesBeenManuallyDeleted(kbnRootPath: string) { + return ( + !(await areNodeModulesPresent(kbnRootPath)) && + (await haveBazelFoldersBeenCreatedBefore(kbnRootPath)) + ); +} diff --git a/packages/kbn-pm/src/utils/bazel/yarn_integrity.ts b/packages/kbn-pm/src/utils/bazel/yarn_integrity.ts deleted file mode 100644 index 1ac9bfeba1e3b..0000000000000 --- a/packages/kbn-pm/src/utils/bazel/yarn_integrity.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { join } from 'path'; -import { isFile, tryRealpath, unlink } from '../fs'; - -export async function removeYarnIntegrityFileIfExists(nodeModulesPath: string) { - try { - const nodeModulesRealPath = await tryRealpath(nodeModulesPath); - const yarnIntegrityFilePath = join(nodeModulesRealPath, '.yarn-integrity'); - - // check if the file exists and delete it in that case - if (await isFile(yarnIntegrityFilePath)) { - await unlink(yarnIntegrityFilePath); - } - } catch { - // no-op - } -} From da2315225c77ef04b09378f859791e66464c8e82 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau Date: Wed, 22 Jun 2022 22:58:49 -0400 Subject: [PATCH 07/54] [RAM] O11y register alert config (#134943) * still lazy load alert configuration but on the start and not teh mount * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * review I Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../register_alerts_table_configuration.tsx | 28 ++++++++----------- x-pack/plugins/observability/public/plugin.ts | 18 ++++++++---- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/observability/public/config/register_alerts_table_configuration.tsx b/x-pack/plugins/observability/public/config/register_alerts_table_configuration.tsx index 2ecaca0eb3e81..c3f02684ab879 100644 --- a/x-pack/plugins/observability/public/config/register_alerts_table_configuration.tsx +++ b/x-pack/plugins/observability/public/config/register_alerts_table_configuration.tsx @@ -6,7 +6,6 @@ */ import type { - AlertsTableConfigurationRegistryContract, AlertTableFlyoutComponent, GetRenderCellValue, } from '@kbn/triggers-actions-ui-plugin/public'; @@ -27,20 +26,15 @@ const AlertsFlyoutFooterLazy = lazy( () => import('../pages/alerts/components/alerts_flyout/alerts_flyout_footer') ); -const registerAlertsTableConfiguration = (registry: AlertsTableConfigurationRegistryContract) => { - if (registry.has(observabilityFeatureId)) { - return; - } - registry.register({ - id: observabilityFeatureId, - columns: alertO11yColumns.map(addDisplayNames), - externalFlyout: { - header: AlertsPageFlyoutHeaderLazy as AlertTableFlyoutComponent, - body: AlertsPageFlyoutBodyLazy as AlertTableFlyoutComponent, - footer: AlertsFlyoutFooterLazy as AlertTableFlyoutComponent, - }, - getRenderCellValue: getRenderCellValue as GetRenderCellValue, - }); -}; +const getO11yAlertsTableConfiguration = () => ({ + id: observabilityFeatureId, + columns: alertO11yColumns.map(addDisplayNames), + externalFlyout: { + header: AlertsPageFlyoutHeaderLazy as AlertTableFlyoutComponent, + body: AlertsPageFlyoutBodyLazy as AlertTableFlyoutComponent, + footer: AlertsFlyoutFooterLazy as AlertTableFlyoutComponent, + }, + getRenderCellValue: getRenderCellValue as GetRenderCellValue, +}); -export { registerAlertsTableConfiguration }; +export { getO11yAlertsTableConfiguration }; diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index d0c7cf3dbad45..20d87b1da4b32 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -144,12 +144,6 @@ export class Plugin // Get start services const [coreStart, pluginsStart, { navigation }] = await coreSetup.getStartServices(); - // Register alerts metadata - const { registerAlertsTableConfiguration } = await import( - './config/register_alerts_table_configuration' - ); - const { alertsTableConfigurationRegistry } = pluginsStart.triggersActionsUi; - registerAlertsTableConfiguration(alertsTableConfigurationRegistry); // The `/api/features` endpoint requires the "Global All" Kibana privilege. Users with a // subset of this privilege are not authorized to access this endpoint and will receive a 404 // error that causes the Alerting view to fail to load. @@ -288,6 +282,18 @@ export class Plugin getSharedUXContext: pluginsStart.sharedUX.getContextServices, }); + const getAsyncO11yAlertsTableConfiguration = async () => { + const { getO11yAlertsTableConfiguration } = await import( + './config/register_alerts_table_configuration' + ); + return getO11yAlertsTableConfiguration(); + }; + + const { alertsTableConfigurationRegistry } = pluginsStart.triggersActionsUi; + getAsyncO11yAlertsTableConfiguration().then((config) => { + alertsTableConfigurationRegistry.register(config); + }); + return { navigation: { PageTemplate, From 52fa9c391fe85a6f6fe5c8e5a361a7735ca1df9e Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 23 Jun 2022 00:50:55 -0400 Subject: [PATCH 08/54] [api-docs] Daily api_docs build (#134973) --- api_docs/actions.mdx | 2 +- api_docs/advanced_settings.mdx | 2 +- api_docs/aiops.mdx | 2 +- api_docs/alerting.mdx | 2 +- api_docs/apm.mdx | 2 +- api_docs/banners.mdx | 2 +- api_docs/bfetch.mdx | 2 +- api_docs/canvas.mdx | 2 +- api_docs/cases.mdx | 2 +- api_docs/charts.mdx | 2 +- api_docs/cloud.mdx | 2 +- api_docs/cloud_security_posture.mdx | 2 +- api_docs/console.mdx | 2 +- api_docs/controls.mdx | 2 +- api_docs/core.devdocs.json | 32 +-- api_docs/core.mdx | 4 +- api_docs/core_application.mdx | 4 +- api_docs/core_chrome.mdx | 4 +- api_docs/core_http.mdx | 4 +- api_docs/core_saved_objects.mdx | 4 +- api_docs/custom_integrations.mdx | 2 +- api_docs/dashboard.mdx | 2 +- api_docs/dashboard_enhanced.mdx | 2 +- api_docs/data.mdx | 2 +- api_docs/data_query.mdx | 2 +- api_docs/data_search.mdx | 2 +- api_docs/data_view_editor.mdx | 2 +- api_docs/data_view_field_editor.mdx | 2 +- api_docs/data_view_management.mdx | 2 +- api_docs/data_views.mdx | 2 +- api_docs/data_visualizer.mdx | 2 +- api_docs/deprecations_by_api.mdx | 2 +- api_docs/deprecations_by_plugin.mdx | 2 +- api_docs/deprecations_by_team.mdx | 2 +- api_docs/dev_tools.mdx | 2 +- api_docs/discover.mdx | 2 +- api_docs/discover_enhanced.mdx | 2 +- api_docs/elastic_apm_synthtrace.mdx | 2 +- api_docs/embeddable.mdx | 2 +- api_docs/embeddable_enhanced.mdx | 2 +- api_docs/encrypted_saved_objects.mdx | 2 +- api_docs/enterprise_search.mdx | 2 +- api_docs/es_ui_shared.mdx | 2 +- api_docs/event_annotation.mdx | 2 +- api_docs/event_log.mdx | 2 +- api_docs/expression_error.mdx | 2 +- api_docs/expression_gauge.mdx | 2 +- api_docs/expression_heatmap.mdx | 2 +- api_docs/expression_image.mdx | 2 +- api_docs/expression_metric.mdx | 2 +- api_docs/expression_metric_vis.mdx | 2 +- api_docs/expression_partition_vis.mdx | 2 +- api_docs/expression_repeat_image.mdx | 2 +- api_docs/expression_reveal_image.mdx | 2 +- api_docs/expression_shape.mdx | 2 +- api_docs/expression_tagcloud.mdx | 2 +- api_docs/expression_x_y.mdx | 2 +- api_docs/expressions.mdx | 2 +- api_docs/features.mdx | 2 +- api_docs/field_formats.mdx | 2 +- api_docs/file_upload.mdx | 2 +- api_docs/fleet.mdx | 2 +- api_docs/global_search.mdx | 2 +- api_docs/home.mdx | 2 +- api_docs/index_lifecycle_management.mdx | 2 +- api_docs/index_management.mdx | 2 +- api_docs/infra.mdx | 2 +- api_docs/inspector.mdx | 2 +- api_docs/interactive_setup.mdx | 2 +- api_docs/kbn_ace.mdx | 2 +- api_docs/kbn_aiops_utils.mdx | 2 +- api_docs/kbn_alerts.mdx | 2 +- api_docs/kbn_analytics.mdx | 2 +- api_docs/kbn_analytics_client.mdx | 2 +- ..._analytics_shippers_elastic_v3_browser.mdx | 2 +- ...n_analytics_shippers_elastic_v3_common.mdx | 2 +- ...n_analytics_shippers_elastic_v3_server.mdx | 2 +- api_docs/kbn_analytics_shippers_fullstory.mdx | 2 +- api_docs/kbn_apm_config_loader.mdx | 2 +- api_docs/kbn_apm_utils.mdx | 2 +- api_docs/kbn_axe_config.mdx | 2 +- api_docs/kbn_bazel_packages.mdx | 2 +- api_docs/kbn_bazel_runner.mdx | 2 +- api_docs/kbn_ci_stats_core.mdx | 2 +- ..._ci_stats_performance_metrics.devdocs.json | 75 +++++ api_docs/kbn_ci_stats_performance_metrics.mdx | 27 ++ api_docs/kbn_ci_stats_reporter.devdocs.json | 32 +++ api_docs/kbn_ci_stats_reporter.mdx | 4 +- api_docs/kbn_cli_dev_mode.mdx | 2 +- api_docs/kbn_coloring.mdx | 2 +- api_docs/kbn_config.mdx | 2 +- api_docs/kbn_config_mocks.mdx | 2 +- api_docs/kbn_config_schema.mdx | 2 +- api_docs/kbn_core_analytics_browser.mdx | 2 +- .../kbn_core_analytics_browser_internal.mdx | 2 +- api_docs/kbn_core_analytics_browser_mocks.mdx | 2 +- .../kbn_core_analytics_server.devdocs.json | 114 ++++++++ api_docs/kbn_core_analytics_server.mdx | 27 ++ ...ore_analytics_server_internal.devdocs.json | 134 +++++++++ .../kbn_core_analytics_server_internal.mdx | 27 ++ ...n_core_analytics_server_mocks.devdocs.json | 107 +++++++ api_docs/kbn_core_analytics_server_mocks.mdx | 27 ++ api_docs/kbn_core_base_browser_mocks.mdx | 2 +- api_docs/kbn_core_base_common.mdx | 2 +- ...kbn_core_base_server_internal.devdocs.json | 123 +++++++++ api_docs/kbn_core_base_server_internal.mdx | 27 ++ api_docs/kbn_core_base_server_mocks.mdx | 2 +- ...n_core_config_server_internal.devdocs.json | 260 ++++++++++++++++++ api_docs/kbn_core_config_server_internal.mdx | 27 ++ api_docs/kbn_core_doc_links_browser.mdx | 2 +- api_docs/kbn_core_doc_links_browser_mocks.mdx | 2 +- api_docs/kbn_core_doc_links_server.mdx | 2 +- api_docs/kbn_core_doc_links_server_mocks.mdx | 2 +- api_docs/kbn_core_i18n_browser.devdocs.json | 86 ++++++ api_docs/kbn_core_i18n_browser.mdx | 27 ++ .../kbn_core_i18n_browser_mocks.devdocs.json | 73 +++++ api_docs/kbn_core_i18n_browser_mocks.mdx | 27 ++ .../kbn_core_injected_metadata_browser.mdx | 2 +- ...n_core_injected_metadata_browser_mocks.mdx | 2 +- api_docs/kbn_core_logging_server.mdx | 2 +- api_docs/kbn_core_logging_server_internal.mdx | 2 +- api_docs/kbn_core_logging_server_mocks.mdx | 2 +- api_docs/kbn_core_theme_browser.mdx | 2 +- api_docs/kbn_core_theme_browser_mocks.mdx | 2 +- api_docs/kbn_crypto.mdx | 2 +- api_docs/kbn_datemath.mdx | 2 +- api_docs/kbn_dev_cli_errors.mdx | 2 +- api_docs/kbn_dev_cli_runner.mdx | 2 +- api_docs/kbn_dev_proc_runner.mdx | 2 +- api_docs/kbn_dev_utils.mdx | 2 +- api_docs/kbn_doc_links.mdx | 2 +- api_docs/kbn_docs_utils.mdx | 2 +- api_docs/kbn_es_archiver.mdx | 2 +- api_docs/kbn_es_query.mdx | 2 +- api_docs/kbn_eslint_plugin_imports.mdx | 2 +- api_docs/kbn_field_types.mdx | 2 +- api_docs/kbn_find_used_node_modules.mdx | 2 +- api_docs/kbn_generate.mdx | 2 +- api_docs/kbn_handlebars.mdx | 2 +- api_docs/kbn_i18n.mdx | 2 +- api_docs/kbn_import_resolver.mdx | 2 +- api_docs/kbn_interpreter.mdx | 2 +- api_docs/kbn_io_ts_utils.mdx | 2 +- api_docs/kbn_jest_serializers.mdx | 2 +- api_docs/kbn_kibana_json_schema.mdx | 2 +- api_docs/kbn_logging.mdx | 2 +- api_docs/kbn_logging_mocks.mdx | 2 +- api_docs/kbn_mapbox_gl.mdx | 2 +- api_docs/kbn_monaco.mdx | 2 +- api_docs/kbn_optimizer.mdx | 2 +- api_docs/kbn_optimizer_webpack_helpers.mdx | 2 +- ..._performance_testing_dataset_extractor.mdx | 2 +- api_docs/kbn_plugin_discovery.mdx | 2 +- api_docs/kbn_plugin_generator.mdx | 2 +- api_docs/kbn_plugin_helpers.mdx | 2 +- api_docs/kbn_pm.mdx | 2 +- api_docs/kbn_react_field.mdx | 2 +- api_docs/kbn_rule_data_utils.mdx | 2 +- .../kbn_scalability_simulation_generator.mdx | 2 +- .../kbn_securitysolution_autocomplete.mdx | 2 +- api_docs/kbn_securitysolution_es_utils.mdx | 2 +- api_docs/kbn_securitysolution_hook_utils.mdx | 2 +- ..._securitysolution_io_ts_alerting_types.mdx | 2 +- .../kbn_securitysolution_io_ts_list_types.mdx | 2 +- api_docs/kbn_securitysolution_io_ts_types.mdx | 2 +- api_docs/kbn_securitysolution_io_ts_utils.mdx | 2 +- api_docs/kbn_securitysolution_list_api.mdx | 2 +- .../kbn_securitysolution_list_constants.mdx | 2 +- api_docs/kbn_securitysolution_list_hooks.mdx | 2 +- api_docs/kbn_securitysolution_list_utils.mdx | 2 +- api_docs/kbn_securitysolution_rules.mdx | 2 +- api_docs/kbn_securitysolution_t_grid.mdx | 2 +- api_docs/kbn_securitysolution_utils.mdx | 2 +- api_docs/kbn_server_http_tools.mdx | 2 +- api_docs/kbn_server_route_repository.mdx | 2 +- api_docs/kbn_shared_ux_button_toolbar.mdx | 2 +- api_docs/kbn_shared_ux_card_no_data.mdx | 2 +- api_docs/kbn_shared_ux_components.mdx | 2 +- .../kbn_shared_ux_page_analytics_no_data.mdx | 2 +- .../kbn_shared_ux_page_kibana_no_data.mdx | 2 +- .../kbn_shared_ux_prompt_no_data_views.mdx | 2 +- api_docs/kbn_shared_ux_services.mdx | 2 +- api_docs/kbn_shared_ux_storybook.mdx | 2 +- api_docs/kbn_shared_ux_utility.mdx | 2 +- api_docs/kbn_sort_package_json.mdx | 2 +- api_docs/kbn_std.mdx | 2 +- api_docs/kbn_stdio_dev_helpers.mdx | 2 +- api_docs/kbn_storybook.mdx | 2 +- api_docs/kbn_telemetry_tools.mdx | 2 +- api_docs/kbn_test.devdocs.json | 81 ++++++ api_docs/kbn_test.mdx | 4 +- api_docs/kbn_test_jest_helpers.mdx | 2 +- api_docs/kbn_tooling_log.mdx | 2 +- api_docs/kbn_type_summarizer.mdx | 2 +- api_docs/kbn_typed_react_router_config.mdx | 2 +- api_docs/kbn_ui_theme.mdx | 2 +- api_docs/kbn_utility_types.mdx | 2 +- api_docs/kbn_utility_types_jest.mdx | 2 +- api_docs/kbn_utils.mdx | 2 +- api_docs/kibana_overview.mdx | 2 +- api_docs/kibana_react.devdocs.json | 8 +- api_docs/kibana_react.mdx | 2 +- api_docs/kibana_utils.mdx | 2 +- api_docs/kubernetes_security.mdx | 2 +- api_docs/lens.mdx | 2 +- api_docs/license_api_guard.mdx | 2 +- api_docs/license_management.mdx | 2 +- api_docs/licensing.mdx | 2 +- api_docs/lists.mdx | 2 +- api_docs/management.mdx | 2 +- api_docs/maps.mdx | 2 +- api_docs/maps_ems.mdx | 2 +- api_docs/ml.mdx | 2 +- api_docs/monitoring.mdx | 2 +- api_docs/monitoring_collection.mdx | 2 +- api_docs/navigation.mdx | 2 +- api_docs/newsfeed.mdx | 2 +- api_docs/observability.mdx | 2 +- api_docs/osquery.mdx | 2 +- api_docs/plugin_directory.mdx | 20 +- api_docs/presentation_util.mdx | 2 +- api_docs/remote_clusters.mdx | 2 +- api_docs/reporting.mdx | 2 +- api_docs/rollup.mdx | 2 +- api_docs/rule_registry.mdx | 2 +- api_docs/runtime_fields.mdx | 2 +- api_docs/saved_objects.mdx | 2 +- api_docs/saved_objects_management.mdx | 2 +- api_docs/saved_objects_tagging.mdx | 2 +- api_docs/saved_objects_tagging_oss.mdx | 2 +- api_docs/screenshot_mode.mdx | 2 +- api_docs/screenshotting.mdx | 2 +- api_docs/security.mdx | 2 +- api_docs/security_solution.mdx | 2 +- api_docs/session_view.mdx | 2 +- api_docs/share.mdx | 2 +- api_docs/shared_u_x.mdx | 2 +- api_docs/snapshot_restore.mdx | 2 +- api_docs/spaces.mdx | 2 +- api_docs/stack_alerts.mdx | 2 +- api_docs/task_manager.mdx | 2 +- api_docs/telemetry.mdx | 2 +- api_docs/telemetry_collection_manager.mdx | 2 +- api_docs/telemetry_collection_xpack.mdx | 2 +- api_docs/telemetry_management_section.mdx | 2 +- api_docs/timelines.mdx | 2 +- api_docs/transform.mdx | 2 +- api_docs/triggers_actions_ui.mdx | 2 +- api_docs/ui_actions.mdx | 2 +- api_docs/ui_actions_enhanced.mdx | 2 +- api_docs/unified_search.mdx | 2 +- api_docs/unified_search_autocomplete.mdx | 2 +- api_docs/url_forwarding.mdx | 2 +- api_docs/usage_collection.mdx | 2 +- api_docs/ux.mdx | 2 +- api_docs/vis_default_editor.mdx | 2 +- api_docs/vis_type_gauge.mdx | 2 +- api_docs/vis_type_heatmap.mdx | 2 +- api_docs/vis_type_pie.mdx | 2 +- api_docs/vis_type_table.mdx | 2 +- api_docs/vis_type_timelion.mdx | 2 +- api_docs/vis_type_timeseries.mdx | 2 +- api_docs/vis_type_vega.mdx | 2 +- api_docs/vis_type_vislib.mdx | 2 +- api_docs/vis_type_xy.mdx | 2 +- api_docs/visualizations.mdx | 2 +- 266 files changed, 1581 insertions(+), 284 deletions(-) create mode 100644 api_docs/kbn_ci_stats_performance_metrics.devdocs.json create mode 100644 api_docs/kbn_ci_stats_performance_metrics.mdx create mode 100644 api_docs/kbn_core_analytics_server.devdocs.json create mode 100644 api_docs/kbn_core_analytics_server.mdx create mode 100644 api_docs/kbn_core_analytics_server_internal.devdocs.json create mode 100644 api_docs/kbn_core_analytics_server_internal.mdx create mode 100644 api_docs/kbn_core_analytics_server_mocks.devdocs.json create mode 100644 api_docs/kbn_core_analytics_server_mocks.mdx create mode 100644 api_docs/kbn_core_base_server_internal.devdocs.json create mode 100644 api_docs/kbn_core_base_server_internal.mdx create mode 100644 api_docs/kbn_core_config_server_internal.devdocs.json create mode 100644 api_docs/kbn_core_config_server_internal.mdx create mode 100644 api_docs/kbn_core_i18n_browser.devdocs.json create mode 100644 api_docs/kbn_core_i18n_browser.mdx create mode 100644 api_docs/kbn_core_i18n_browser_mocks.devdocs.json create mode 100644 api_docs/kbn_core_i18n_browser_mocks.mdx diff --git a/api_docs/actions.mdx b/api_docs/actions.mdx index d797669651470..aa8d70b3f87a3 100644 --- a/api_docs/actions.mdx +++ b/api_docs/actions.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/actions title: "actions" image: https://source.unsplash.com/400x175/?github summary: API docs for the actions plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'actions'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/advanced_settings.mdx b/api_docs/advanced_settings.mdx index 1b8402f59c88f..b4359ef0e6fac 100644 --- a/api_docs/advanced_settings.mdx +++ b/api_docs/advanced_settings.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/advancedSettings title: "advancedSettings" image: https://source.unsplash.com/400x175/?github summary: API docs for the advancedSettings plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'advancedSettings'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/aiops.mdx b/api_docs/aiops.mdx index e970547c6ae93..b84273128b873 100644 --- a/api_docs/aiops.mdx +++ b/api_docs/aiops.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/aiops title: "aiops" image: https://source.unsplash.com/400x175/?github summary: API docs for the aiops plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'aiops'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/alerting.mdx b/api_docs/alerting.mdx index 4cbf27c578880..c660f149a18f7 100644 --- a/api_docs/alerting.mdx +++ b/api_docs/alerting.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/alerting title: "alerting" image: https://source.unsplash.com/400x175/?github summary: API docs for the alerting plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'alerting'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/apm.mdx b/api_docs/apm.mdx index 51bd8b5ba4213..6100122ca8cb1 100644 --- a/api_docs/apm.mdx +++ b/api_docs/apm.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/apm title: "apm" image: https://source.unsplash.com/400x175/?github summary: API docs for the apm plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'apm'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/banners.mdx b/api_docs/banners.mdx index 2bf8f6d1b2d51..9efb27bdf70d3 100644 --- a/api_docs/banners.mdx +++ b/api_docs/banners.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/banners title: "banners" image: https://source.unsplash.com/400x175/?github summary: API docs for the banners plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'banners'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/bfetch.mdx b/api_docs/bfetch.mdx index b30e2db5d784f..75a4484d9dbca 100644 --- a/api_docs/bfetch.mdx +++ b/api_docs/bfetch.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/bfetch title: "bfetch" image: https://source.unsplash.com/400x175/?github summary: API docs for the bfetch plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'bfetch'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/canvas.mdx b/api_docs/canvas.mdx index 447683f585321..d15bfd331d410 100644 --- a/api_docs/canvas.mdx +++ b/api_docs/canvas.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/canvas title: "canvas" image: https://source.unsplash.com/400x175/?github summary: API docs for the canvas plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'canvas'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/cases.mdx b/api_docs/cases.mdx index 01740e93b83fc..8e6b7ff04c30a 100644 --- a/api_docs/cases.mdx +++ b/api_docs/cases.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/cases title: "cases" image: https://source.unsplash.com/400x175/?github summary: API docs for the cases plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cases'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/charts.mdx b/api_docs/charts.mdx index e3255cd38c663..2d9fe262fab2b 100644 --- a/api_docs/charts.mdx +++ b/api_docs/charts.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/charts title: "charts" image: https://source.unsplash.com/400x175/?github summary: API docs for the charts plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'charts'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/cloud.mdx b/api_docs/cloud.mdx index 1e52786a522d5..015725fc4b46b 100644 --- a/api_docs/cloud.mdx +++ b/api_docs/cloud.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/cloud title: "cloud" image: https://source.unsplash.com/400x175/?github summary: API docs for the cloud plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloud'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/cloud_security_posture.mdx b/api_docs/cloud_security_posture.mdx index 2886ff662f997..552c5f84bbb6a 100644 --- a/api_docs/cloud_security_posture.mdx +++ b/api_docs/cloud_security_posture.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/cloudSecurityPosture title: "cloudSecurityPosture" image: https://source.unsplash.com/400x175/?github summary: API docs for the cloudSecurityPosture plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudSecurityPosture'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/console.mdx b/api_docs/console.mdx index f9595356a14ce..ecd66045beb20 100644 --- a/api_docs/console.mdx +++ b/api_docs/console.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/console title: "console" image: https://source.unsplash.com/400x175/?github summary: API docs for the console plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'console'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/controls.mdx b/api_docs/controls.mdx index 67009a44f3de8..67400c3730c05 100644 --- a/api_docs/controls.mdx +++ b/api_docs/controls.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/controls title: "controls" image: https://source.unsplash.com/400x175/?github summary: API docs for the controls plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'controls'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/core.devdocs.json b/api_docs/core.devdocs.json index 78f4a5a65e08d..c1181b59c80c0 100644 --- a/api_docs/core.devdocs.json +++ b/api_docs/core.devdocs.json @@ -1583,13 +1583,7 @@ "{@link I18nStart}" ], "signature": [ - { - "pluginId": "core", - "scope": "public", - "docId": "kibCorePluginApi", - "section": "def-public.I18nStart", - "text": "I18nStart" - } + "I18nStart" ], "path": "src/core/public/index.ts", "deprecated": false @@ -2647,9 +2641,9 @@ "tags": [], "label": "I18nStart", "description": [ - "\nI18nStart.Context is required by any localizable React component from \\@kbn/i18n and \\@elastic/eui packages\nand is supposed to be used as the topmost component for any i18n-compatible React tree.\n" + "\r\nI18nStart.Context is required by any localizable React component from \\@kbn/i18n and \\@elastic/eui packages\r\nand is supposed to be used as the topmost component for any i18n-compatible React tree.\r\n" ], - "path": "src/core/public/i18n/i18n_service.tsx", + "path": "node_modules/@types/kbn__core-i18n-browser/index.d.ts", "deprecated": false, "children": [ { @@ -2659,12 +2653,12 @@ "tags": [], "label": "Context", "description": [ - "\nReact Context provider required as the topmost component for any i18n-compatible React tree." + "\r\nReact Context provider required as the topmost component for any i18n-compatible React tree." ], "signature": [ "({ children }: { children: React.ReactNode; }) => JSX.Element" ], - "path": "src/core/public/i18n/i18n_service.tsx", + "path": "node_modules/@types/kbn__core-i18n-browser/index.d.ts", "deprecated": false, "children": [ { @@ -2674,7 +2668,7 @@ "tags": [], "label": "{ children }", "description": [], - "path": "src/core/public/i18n/i18n_service.tsx", + "path": "node_modules/@types/kbn__core-i18n-browser/index.d.ts", "deprecated": false, "children": [ { @@ -2687,7 +2681,7 @@ "signature": [ "boolean | React.ReactChild | React.ReactFragment | React.ReactPortal | null | undefined" ], - "path": "src/core/public/i18n/i18n_service.tsx", + "path": "node_modules/@types/kbn__core-i18n-browser/index.d.ts", "deprecated": false } ] @@ -24176,7 +24170,7 @@ "tags": [], "label": "AnalyticsServicePreboot", "description": [ - "\nExposes the public APIs of the AnalyticsClient during the preboot phase\n{@link AnalyticsClient}" + "\r\nExposes the public APIs of the AnalyticsClient during the preboot phase\r\n{@link AnalyticsClient}" ], "signature": [ "{ optIn: (optInConfig: ", @@ -24197,7 +24191,7 @@ "ContextProviderOpts", ") => void; removeContextProvider: (contextProviderName: string) => void; }" ], - "path": "src/core/server/analytics/analytics_service.ts", + "path": "node_modules/@types/kbn__core-analytics-server/index.d.ts", "deprecated": false, "initialIsOpen": false }, @@ -24208,7 +24202,7 @@ "tags": [], "label": "AnalyticsServiceSetup", "description": [ - "\nExposes the public APIs of the AnalyticsClient during the setup phase.\n{@link AnalyticsClient}" + "\r\nExposes the public APIs of the AnalyticsClient during the setup phase.\r\n{@link AnalyticsClient}" ], "signature": [ "{ optIn: (optInConfig: ", @@ -24229,7 +24223,7 @@ "ContextProviderOpts", ") => void; removeContextProvider: (contextProviderName: string) => void; }" ], - "path": "src/core/server/analytics/analytics_service.ts", + "path": "node_modules/@types/kbn__core-analytics-server/index.d.ts", "deprecated": false, "initialIsOpen": false }, @@ -24240,7 +24234,7 @@ "tags": [], "label": "AnalyticsServiceStart", "description": [ - "\nExposes the public APIs of the AnalyticsClient during the start phase\n{@link AnalyticsClient}" + "\r\nExposes the public APIs of the AnalyticsClient during the start phase\r\n{@link AnalyticsClient}" ], "signature": [ "{ optIn: (optInConfig: ", @@ -24251,7 +24245,7 @@ "TelemetryCounter", ">; }" ], - "path": "src/core/server/analytics/analytics_service.ts", + "path": "node_modules/@types/kbn__core-analytics-server/index.d.ts", "deprecated": false, "initialIsOpen": false }, diff --git a/api_docs/core.mdx b/api_docs/core.mdx index 9bf639c6ceed3..e3b2da5c430d1 100644 --- a/api_docs/core.mdx +++ b/api_docs/core.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/core title: "core" image: https://source.unsplash.com/400x175/?github summary: API docs for the core plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'core'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- @@ -18,7 +18,7 @@ Contact [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) for que | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 2527 | 15 | 940 | 29 | +| 2527 | 15 | 938 | 29 | ## Client diff --git a/api_docs/core_application.mdx b/api_docs/core_application.mdx index 9b15a4c99728b..ccd83243240cc 100644 --- a/api_docs/core_application.mdx +++ b/api_docs/core_application.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/core-application title: "core.application" image: https://source.unsplash.com/400x175/?github summary: API docs for the core.application plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'core.application'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- @@ -18,7 +18,7 @@ Contact [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) for que | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 2527 | 15 | 940 | 29 | +| 2527 | 15 | 938 | 29 | ## Client diff --git a/api_docs/core_chrome.mdx b/api_docs/core_chrome.mdx index 8ecc59ccbebed..a9682a8e3b4a1 100644 --- a/api_docs/core_chrome.mdx +++ b/api_docs/core_chrome.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/core-chrome title: "core.chrome" image: https://source.unsplash.com/400x175/?github summary: API docs for the core.chrome plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'core.chrome'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- @@ -18,7 +18,7 @@ Contact [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) for que | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 2527 | 15 | 940 | 29 | +| 2527 | 15 | 938 | 29 | ## Client diff --git a/api_docs/core_http.mdx b/api_docs/core_http.mdx index e674a58a1557d..dbe127e3f003d 100644 --- a/api_docs/core_http.mdx +++ b/api_docs/core_http.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/core-http title: "core.http" image: https://source.unsplash.com/400x175/?github summary: API docs for the core.http plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'core.http'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- @@ -18,7 +18,7 @@ Contact [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) for que | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 2527 | 15 | 940 | 29 | +| 2527 | 15 | 938 | 29 | ## Client diff --git a/api_docs/core_saved_objects.mdx b/api_docs/core_saved_objects.mdx index 04a7d497d1b01..a4443c941dd86 100644 --- a/api_docs/core_saved_objects.mdx +++ b/api_docs/core_saved_objects.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/core-savedObjects title: "core.savedObjects" image: https://source.unsplash.com/400x175/?github summary: API docs for the core.savedObjects plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'core.savedObjects'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- @@ -18,7 +18,7 @@ Contact [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) for que | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 2527 | 15 | 940 | 29 | +| 2527 | 15 | 938 | 29 | ## Client diff --git a/api_docs/custom_integrations.mdx b/api_docs/custom_integrations.mdx index f04be45f934fe..f7586442664bc 100644 --- a/api_docs/custom_integrations.mdx +++ b/api_docs/custom_integrations.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/customIntegrations title: "customIntegrations" image: https://source.unsplash.com/400x175/?github summary: API docs for the customIntegrations plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'customIntegrations'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/dashboard.mdx b/api_docs/dashboard.mdx index 98dea779023b5..8dc6d7fdcfb6f 100644 --- a/api_docs/dashboard.mdx +++ b/api_docs/dashboard.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/dashboard title: "dashboard" image: https://source.unsplash.com/400x175/?github summary: API docs for the dashboard plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboard'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/dashboard_enhanced.mdx b/api_docs/dashboard_enhanced.mdx index cea698f443ce0..e8953c4281e28 100644 --- a/api_docs/dashboard_enhanced.mdx +++ b/api_docs/dashboard_enhanced.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/dashboardEnhanced title: "dashboardEnhanced" image: https://source.unsplash.com/400x175/?github summary: API docs for the dashboardEnhanced plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboardEnhanced'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/data.mdx b/api_docs/data.mdx index cc6c86fcb5f52..9e2ff20a26ccb 100644 --- a/api_docs/data.mdx +++ b/api_docs/data.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/data title: "data" image: https://source.unsplash.com/400x175/?github summary: API docs for the data plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/data_query.mdx b/api_docs/data_query.mdx index a7bb8fa4d9919..97cb36245b0df 100644 --- a/api_docs/data_query.mdx +++ b/api_docs/data_query.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/data-query title: "data.query" image: https://source.unsplash.com/400x175/?github summary: API docs for the data.query plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.query'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/data_search.mdx b/api_docs/data_search.mdx index bde8c91920935..badce42c47a6f 100644 --- a/api_docs/data_search.mdx +++ b/api_docs/data_search.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/data-search title: "data.search" image: https://source.unsplash.com/400x175/?github summary: API docs for the data.search plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.search'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/data_view_editor.mdx b/api_docs/data_view_editor.mdx index f62aa9f36c3c1..6ff647d20d447 100644 --- a/api_docs/data_view_editor.mdx +++ b/api_docs/data_view_editor.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/dataViewEditor title: "dataViewEditor" image: https://source.unsplash.com/400x175/?github summary: API docs for the dataViewEditor plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewEditor'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/data_view_field_editor.mdx b/api_docs/data_view_field_editor.mdx index 0943418c1c5b2..ce0cac3a3ace3 100644 --- a/api_docs/data_view_field_editor.mdx +++ b/api_docs/data_view_field_editor.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/dataViewFieldEditor title: "dataViewFieldEditor" image: https://source.unsplash.com/400x175/?github summary: API docs for the dataViewFieldEditor plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewFieldEditor'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/data_view_management.mdx b/api_docs/data_view_management.mdx index 4297e16c393fc..8a0db6bb2066e 100644 --- a/api_docs/data_view_management.mdx +++ b/api_docs/data_view_management.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/dataViewManagement title: "dataViewManagement" image: https://source.unsplash.com/400x175/?github summary: API docs for the dataViewManagement plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewManagement'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/data_views.mdx b/api_docs/data_views.mdx index 3d50dcf400d63..8d2ffe07ca1e9 100644 --- a/api_docs/data_views.mdx +++ b/api_docs/data_views.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/dataViews title: "dataViews" image: https://source.unsplash.com/400x175/?github summary: API docs for the dataViews plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViews'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/data_visualizer.mdx b/api_docs/data_visualizer.mdx index d545aa1752f9c..4761b678c4b82 100644 --- a/api_docs/data_visualizer.mdx +++ b/api_docs/data_visualizer.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/dataVisualizer title: "dataVisualizer" image: https://source.unsplash.com/400x175/?github summary: API docs for the dataVisualizer plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataVisualizer'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/deprecations_by_api.mdx b/api_docs/deprecations_by_api.mdx index 97c708c366bd2..5683d95bd30fe 100644 --- a/api_docs/deprecations_by_api.mdx +++ b/api_docs/deprecations_by_api.mdx @@ -3,7 +3,7 @@ id: kibDevDocsDeprecationsByApi slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-api title: Deprecated API usage by API summary: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. --- diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx index d4222f6c65916..75ca037c25bb2 100644 --- a/api_docs/deprecations_by_plugin.mdx +++ b/api_docs/deprecations_by_plugin.mdx @@ -3,7 +3,7 @@ id: kibDevDocsDeprecationsByPlugin slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-plugin title: Deprecated API usage by plugin summary: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. --- diff --git a/api_docs/deprecations_by_team.mdx b/api_docs/deprecations_by_team.mdx index 9a995d287dcfe..38244e76f344c 100644 --- a/api_docs/deprecations_by_team.mdx +++ b/api_docs/deprecations_by_team.mdx @@ -3,7 +3,7 @@ id: kibDevDocsDeprecationsDueByTeam slug: /kibana-dev-docs/api-meta/deprecations-due-by-team title: Deprecated APIs due to be removed, by team summary: Lists the teams that are referencing deprecated APIs with a remove by date. -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. --- diff --git a/api_docs/dev_tools.mdx b/api_docs/dev_tools.mdx index b95162c73b70c..bca811b137130 100644 --- a/api_docs/dev_tools.mdx +++ b/api_docs/dev_tools.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/devTools title: "devTools" image: https://source.unsplash.com/400x175/?github summary: API docs for the devTools plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'devTools'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/discover.mdx b/api_docs/discover.mdx index 92b15fa4eb518..738d6255dc1fd 100644 --- a/api_docs/discover.mdx +++ b/api_docs/discover.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/discover title: "discover" image: https://source.unsplash.com/400x175/?github summary: API docs for the discover plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discover'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/discover_enhanced.mdx b/api_docs/discover_enhanced.mdx index 2615d10e20873..afd26d8f4c263 100644 --- a/api_docs/discover_enhanced.mdx +++ b/api_docs/discover_enhanced.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/discoverEnhanced title: "discoverEnhanced" image: https://source.unsplash.com/400x175/?github summary: API docs for the discoverEnhanced plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discoverEnhanced'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/elastic_apm_synthtrace.mdx b/api_docs/elastic_apm_synthtrace.mdx index 0dcc042b805fe..bf80df96fbcc5 100644 --- a/api_docs/elastic_apm_synthtrace.mdx +++ b/api_docs/elastic_apm_synthtrace.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/elastic-apm-synthtrace title: "@elastic/apm-synthtrace" image: https://source.unsplash.com/400x175/?github summary: API docs for the @elastic/apm-synthtrace plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@elastic/apm-synthtrace'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/embeddable.mdx b/api_docs/embeddable.mdx index b373d348d2e00..ae626a79b9444 100644 --- a/api_docs/embeddable.mdx +++ b/api_docs/embeddable.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/embeddable title: "embeddable" image: https://source.unsplash.com/400x175/?github summary: API docs for the embeddable plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddable'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/embeddable_enhanced.mdx b/api_docs/embeddable_enhanced.mdx index 87ed46ed5618f..7a04080e707ce 100644 --- a/api_docs/embeddable_enhanced.mdx +++ b/api_docs/embeddable_enhanced.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/embeddableEnhanced title: "embeddableEnhanced" image: https://source.unsplash.com/400x175/?github summary: API docs for the embeddableEnhanced plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddableEnhanced'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/encrypted_saved_objects.mdx b/api_docs/encrypted_saved_objects.mdx index 5d0b699131f19..f80624d76f739 100644 --- a/api_docs/encrypted_saved_objects.mdx +++ b/api_docs/encrypted_saved_objects.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/encryptedSavedObjects title: "encryptedSavedObjects" image: https://source.unsplash.com/400x175/?github summary: API docs for the encryptedSavedObjects plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'encryptedSavedObjects'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/enterprise_search.mdx b/api_docs/enterprise_search.mdx index 2257e89887682..89c9d98c5a85b 100644 --- a/api_docs/enterprise_search.mdx +++ b/api_docs/enterprise_search.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/enterpriseSearch title: "enterpriseSearch" image: https://source.unsplash.com/400x175/?github summary: API docs for the enterpriseSearch plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'enterpriseSearch'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/es_ui_shared.mdx b/api_docs/es_ui_shared.mdx index 7c74fd18d9d7d..71748ba2c183e 100644 --- a/api_docs/es_ui_shared.mdx +++ b/api_docs/es_ui_shared.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/esUiShared title: "esUiShared" image: https://source.unsplash.com/400x175/?github summary: API docs for the esUiShared plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'esUiShared'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/event_annotation.mdx b/api_docs/event_annotation.mdx index db4361f4c71d2..5c7f4d4392176 100644 --- a/api_docs/event_annotation.mdx +++ b/api_docs/event_annotation.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/eventAnnotation title: "eventAnnotation" image: https://source.unsplash.com/400x175/?github summary: API docs for the eventAnnotation plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventAnnotation'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/event_log.mdx b/api_docs/event_log.mdx index ce71219c77661..c0f5673c3e4a5 100644 --- a/api_docs/event_log.mdx +++ b/api_docs/event_log.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/eventLog title: "eventLog" image: https://source.unsplash.com/400x175/?github summary: API docs for the eventLog plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventLog'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_error.mdx b/api_docs/expression_error.mdx index ea3154c081499..972a3099e2ee5 100644 --- a/api_docs/expression_error.mdx +++ b/api_docs/expression_error.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionError title: "expressionError" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionError plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionError'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_gauge.mdx b/api_docs/expression_gauge.mdx index 27f553c9df86f..c738384c60305 100644 --- a/api_docs/expression_gauge.mdx +++ b/api_docs/expression_gauge.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionGauge title: "expressionGauge" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionGauge plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionGauge'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_heatmap.mdx b/api_docs/expression_heatmap.mdx index abff740b84120..aa27f9360b628 100644 --- a/api_docs/expression_heatmap.mdx +++ b/api_docs/expression_heatmap.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionHeatmap title: "expressionHeatmap" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionHeatmap plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionHeatmap'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_image.mdx b/api_docs/expression_image.mdx index 66c0685e867d2..452d5166377c2 100644 --- a/api_docs/expression_image.mdx +++ b/api_docs/expression_image.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionImage title: "expressionImage" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionImage plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionImage'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_metric.mdx b/api_docs/expression_metric.mdx index bfd39bb2af47f..ceb63f6fdc9d5 100644 --- a/api_docs/expression_metric.mdx +++ b/api_docs/expression_metric.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionMetric title: "expressionMetric" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionMetric plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetric'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_metric_vis.mdx b/api_docs/expression_metric_vis.mdx index 218a7ee0ce7ef..be5ffab9f5e45 100644 --- a/api_docs/expression_metric_vis.mdx +++ b/api_docs/expression_metric_vis.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionMetricVis title: "expressionMetricVis" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionMetricVis plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetricVis'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_partition_vis.mdx b/api_docs/expression_partition_vis.mdx index 6469a974d591b..5260a7add6010 100644 --- a/api_docs/expression_partition_vis.mdx +++ b/api_docs/expression_partition_vis.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionPartitionVis title: "expressionPartitionVis" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionPartitionVis plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionPartitionVis'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_repeat_image.mdx b/api_docs/expression_repeat_image.mdx index e9892fab347b0..51bd5c3790838 100644 --- a/api_docs/expression_repeat_image.mdx +++ b/api_docs/expression_repeat_image.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionRepeatImage title: "expressionRepeatImage" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionRepeatImage plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRepeatImage'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_reveal_image.mdx b/api_docs/expression_reveal_image.mdx index 55c6ed9527d46..7e9db0b0f0c70 100644 --- a/api_docs/expression_reveal_image.mdx +++ b/api_docs/expression_reveal_image.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionRevealImage title: "expressionRevealImage" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionRevealImage plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRevealImage'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_shape.mdx b/api_docs/expression_shape.mdx index 202e4c6e2e365..cf4f2a7ae3ebb 100644 --- a/api_docs/expression_shape.mdx +++ b/api_docs/expression_shape.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionShape title: "expressionShape" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionShape plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionShape'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_tagcloud.mdx b/api_docs/expression_tagcloud.mdx index 142c613588dd3..4f8f87e51555d 100644 --- a/api_docs/expression_tagcloud.mdx +++ b/api_docs/expression_tagcloud.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionTagcloud title: "expressionTagcloud" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionTagcloud plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionTagcloud'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_x_y.mdx b/api_docs/expression_x_y.mdx index 6a2bb59b4fea3..bc48042555b6f 100644 --- a/api_docs/expression_x_y.mdx +++ b/api_docs/expression_x_y.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionXY title: "expressionXY" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionXY plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionXY'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expressions.mdx b/api_docs/expressions.mdx index 02e63b832df83..7ceb36224bb14 100644 --- a/api_docs/expressions.mdx +++ b/api_docs/expressions.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressions title: "expressions" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressions plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressions'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/features.mdx b/api_docs/features.mdx index bde45470db66b..f66bc3d55d9a3 100644 --- a/api_docs/features.mdx +++ b/api_docs/features.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/features title: "features" image: https://source.unsplash.com/400x175/?github summary: API docs for the features plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'features'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/field_formats.mdx b/api_docs/field_formats.mdx index aee6e9622505a..5021dd2eea449 100644 --- a/api_docs/field_formats.mdx +++ b/api_docs/field_formats.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/fieldFormats title: "fieldFormats" image: https://source.unsplash.com/400x175/?github summary: API docs for the fieldFormats plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fieldFormats'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/file_upload.mdx b/api_docs/file_upload.mdx index ff81f3660218f..729b43555dc75 100644 --- a/api_docs/file_upload.mdx +++ b/api_docs/file_upload.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/fileUpload title: "fileUpload" image: https://source.unsplash.com/400x175/?github summary: API docs for the fileUpload plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fileUpload'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/fleet.mdx b/api_docs/fleet.mdx index 74ab074a74209..3bb51cbf90196 100644 --- a/api_docs/fleet.mdx +++ b/api_docs/fleet.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/fleet title: "fleet" image: https://source.unsplash.com/400x175/?github summary: API docs for the fleet plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fleet'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/global_search.mdx b/api_docs/global_search.mdx index c9055be152e4c..2f2009cdb8a90 100644 --- a/api_docs/global_search.mdx +++ b/api_docs/global_search.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/globalSearch title: "globalSearch" image: https://source.unsplash.com/400x175/?github summary: API docs for the globalSearch plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'globalSearch'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/home.mdx b/api_docs/home.mdx index 9bf1f688986f6..bc34d629ae8ac 100644 --- a/api_docs/home.mdx +++ b/api_docs/home.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/home title: "home" image: https://source.unsplash.com/400x175/?github summary: API docs for the home plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'home'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/index_lifecycle_management.mdx b/api_docs/index_lifecycle_management.mdx index f2e9c6bfc9a36..1f34605863969 100644 --- a/api_docs/index_lifecycle_management.mdx +++ b/api_docs/index_lifecycle_management.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/indexLifecycleManagement title: "indexLifecycleManagement" image: https://source.unsplash.com/400x175/?github summary: API docs for the indexLifecycleManagement plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexLifecycleManagement'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/index_management.mdx b/api_docs/index_management.mdx index 46baf65ed5268..29f9caea1d449 100644 --- a/api_docs/index_management.mdx +++ b/api_docs/index_management.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/indexManagement title: "indexManagement" image: https://source.unsplash.com/400x175/?github summary: API docs for the indexManagement plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexManagement'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/infra.mdx b/api_docs/infra.mdx index fb6ced4363fcf..fb3cb5242d85c 100644 --- a/api_docs/infra.mdx +++ b/api_docs/infra.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/infra title: "infra" image: https://source.unsplash.com/400x175/?github summary: API docs for the infra plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'infra'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/inspector.mdx b/api_docs/inspector.mdx index a1fc5bd30b20e..95f83c9fa5bc1 100644 --- a/api_docs/inspector.mdx +++ b/api_docs/inspector.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/inspector title: "inspector" image: https://source.unsplash.com/400x175/?github summary: API docs for the inspector plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'inspector'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/interactive_setup.mdx b/api_docs/interactive_setup.mdx index 6c47cc38b0f38..7edb48d5d7919 100644 --- a/api_docs/interactive_setup.mdx +++ b/api_docs/interactive_setup.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/interactiveSetup title: "interactiveSetup" image: https://source.unsplash.com/400x175/?github summary: API docs for the interactiveSetup plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'interactiveSetup'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_ace.mdx b/api_docs/kbn_ace.mdx index a4993cf778bb3..f78923593d2ec 100644 --- a/api_docs/kbn_ace.mdx +++ b/api_docs/kbn_ace.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-ace title: "@kbn/ace" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/ace plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ace'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_aiops_utils.mdx b/api_docs/kbn_aiops_utils.mdx index 37fc706f762bd..c44c318c2ff69 100644 --- a/api_docs/kbn_aiops_utils.mdx +++ b/api_docs/kbn_aiops_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-utils title: "@kbn/aiops-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/aiops-utils plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_alerts.mdx b/api_docs/kbn_alerts.mdx index 8007be27038fc..8226fde3fee3a 100644 --- a/api_docs/kbn_alerts.mdx +++ b/api_docs/kbn_alerts.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-alerts title: "@kbn/alerts" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/alerts plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_analytics.mdx b/api_docs/kbn_analytics.mdx index 3e6c179cb6fd8..f44200ef97bfe 100644 --- a/api_docs/kbn_analytics.mdx +++ b/api_docs/kbn_analytics.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-analytics title: "@kbn/analytics" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/analytics plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_analytics_client.mdx b/api_docs/kbn_analytics_client.mdx index 4018c2960bb47..84315fff01c2c 100644 --- a/api_docs/kbn_analytics_client.mdx +++ b/api_docs/kbn_analytics_client.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-client title: "@kbn/analytics-client" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/analytics-client plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-client'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx index f9efcde37ff5a..47602a63dcc83 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-browser title: "@kbn/analytics-shippers-elastic-v3-browser" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/analytics-shippers-elastic-v3-browser plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-browser'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx index 20f03a5c1b9be..e061874d47be4 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-common title: "@kbn/analytics-shippers-elastic-v3-common" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/analytics-shippers-elastic-v3-common plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-common'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx index 036dff2834a98..4fe9239d5d670 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-server title: "@kbn/analytics-shippers-elastic-v3-server" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/analytics-shippers-elastic-v3-server plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-server'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_analytics_shippers_fullstory.mdx b/api_docs/kbn_analytics_shippers_fullstory.mdx index f20a328bbd061..72814b9f92bea 100644 --- a/api_docs/kbn_analytics_shippers_fullstory.mdx +++ b/api_docs/kbn_analytics_shippers_fullstory.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-fullstory title: "@kbn/analytics-shippers-fullstory" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/analytics-shippers-fullstory plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-fullstory'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_apm_config_loader.mdx b/api_docs/kbn_apm_config_loader.mdx index 8c1d4205d47e4..e0ebf664398e4 100644 --- a/api_docs/kbn_apm_config_loader.mdx +++ b/api_docs/kbn_apm_config_loader.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-apm-config-loader title: "@kbn/apm-config-loader" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/apm-config-loader plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-config-loader'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_apm_utils.mdx b/api_docs/kbn_apm_utils.mdx index 7e4cc478ad446..fe2c5f5459cfb 100644 --- a/api_docs/kbn_apm_utils.mdx +++ b/api_docs/kbn_apm_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-apm-utils title: "@kbn/apm-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/apm-utils plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_axe_config.mdx b/api_docs/kbn_axe_config.mdx index f88ba9abb50f1..c155bf54291d8 100644 --- a/api_docs/kbn_axe_config.mdx +++ b/api_docs/kbn_axe_config.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-axe-config title: "@kbn/axe-config" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/axe-config plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/axe-config'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_bazel_packages.mdx b/api_docs/kbn_bazel_packages.mdx index 20abe35be350f..4fbac5adf9133 100644 --- a/api_docs/kbn_bazel_packages.mdx +++ b/api_docs/kbn_bazel_packages.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-bazel-packages title: "@kbn/bazel-packages" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/bazel-packages plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/bazel-packages'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_bazel_runner.mdx b/api_docs/kbn_bazel_runner.mdx index 3fbcc63a1e034..82ceb55e52a60 100644 --- a/api_docs/kbn_bazel_runner.mdx +++ b/api_docs/kbn_bazel_runner.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-bazel-runner title: "@kbn/bazel-runner" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/bazel-runner plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/bazel-runner'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_ci_stats_core.mdx b/api_docs/kbn_ci_stats_core.mdx index 1a43b788c1825..319f32b8fcb66 100644 --- a/api_docs/kbn_ci_stats_core.mdx +++ b/api_docs/kbn_ci_stats_core.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-core title: "@kbn/ci-stats-core" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/ci-stats-core plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-core'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_ci_stats_performance_metrics.devdocs.json b/api_docs/kbn_ci_stats_performance_metrics.devdocs.json new file mode 100644 index 0000000000000..87ed9278a28f4 --- /dev/null +++ b/api_docs/kbn_ci_stats_performance_metrics.devdocs.json @@ -0,0 +1,75 @@ +{ + "id": "@kbn/ci-stats-performance-metrics", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [ + { + "parentPluginId": "@kbn/ci-stats-performance-metrics", + "id": "def-server.reporter", + "type": "Function", + "tags": [], + "label": "reporter", + "description": [], + "signature": [ + "(options: ReporterOptions) => Promise" + ], + "path": "packages/kbn-ci-stats-performance-metrics/src/reporter.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/ci-stats-performance-metrics", + "id": "def-server.reporter.$1", + "type": "Object", + "tags": [], + "label": "options", + "description": [], + "signature": [ + "ReporterOptions" + ], + "path": "packages/kbn-ci-stats-performance-metrics/src/reporter.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ci-stats-performance-metrics", + "id": "def-server.runCli", + "type": "Function", + "tags": [], + "label": "runCli", + "description": [], + "signature": [ + "() => Promise" + ], + "path": "packages/kbn-ci-stats-performance-metrics/src/cli.ts", + "deprecated": false, + "children": [], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_ci_stats_performance_metrics.mdx b/api_docs/kbn_ci_stats_performance_metrics.mdx new file mode 100644 index 0000000000000..7c492a4f3179f --- /dev/null +++ b/api_docs/kbn_ci_stats_performance_metrics.mdx @@ -0,0 +1,27 @@ +--- +id: kibKbnCiStatsPerformanceMetricsPluginApi +slug: /kibana-dev-docs/api/kbn-ci-stats-performance-metrics +title: "@kbn/ci-stats-performance-metrics" +image: https://source.unsplash.com/400x175/?github +summary: API docs for the @kbn/ci-stats-performance-metrics plugin +date: 2022-06-23 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-performance-metrics'] +warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. +--- +import kbnCiStatsPerformanceMetricsObj from './kbn_ci_stats_performance_metrics.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 3 | 0 | 3 | 0 | + +## Server + +### Functions + + diff --git a/api_docs/kbn_ci_stats_reporter.devdocs.json b/api_docs/kbn_ci_stats_reporter.devdocs.json index 7c0232b6d81dd..195e8e8a5a429 100644 --- a/api_docs/kbn_ci_stats_reporter.devdocs.json +++ b/api_docs/kbn_ci_stats_reporter.devdocs.json @@ -309,6 +309,38 @@ } ], "returnComment": [] + }, + { + "parentPluginId": "@kbn/ci-stats-reporter", + "id": "def-server.CiStatsReporter.reportPerformanceMetrics", + "type": "Function", + "tags": [], + "label": "reportPerformanceMetrics", + "description": [], + "signature": [ + "(metrics: ", + "PerformanceMetrics", + ") => Promise" + ], + "path": "packages/kbn-ci-stats-reporter/src/ci_stats_reporter.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/ci-stats-reporter", + "id": "def-server.CiStatsReporter.reportPerformanceMetrics.$1", + "type": "Object", + "tags": [], + "label": "metrics", + "description": [], + "signature": [ + "PerformanceMetrics" + ], + "path": "packages/kbn-ci-stats-reporter/src/ci_stats_reporter.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [] } ], "initialIsOpen": false diff --git a/api_docs/kbn_ci_stats_reporter.mdx b/api_docs/kbn_ci_stats_reporter.mdx index fd3d10a85a509..8bfd1bacb1ebc 100644 --- a/api_docs/kbn_ci_stats_reporter.mdx +++ b/api_docs/kbn_ci_stats_reporter.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-reporter title: "@kbn/ci-stats-reporter" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/ci-stats-reporter plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-reporter'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- @@ -18,7 +18,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 60 | 0 | 15 | 0 | +| 62 | 0 | 17 | 1 | ## Server diff --git a/api_docs/kbn_cli_dev_mode.mdx b/api_docs/kbn_cli_dev_mode.mdx index 43f72e4163575..d14d05cb69613 100644 --- a/api_docs/kbn_cli_dev_mode.mdx +++ b/api_docs/kbn_cli_dev_mode.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-cli-dev-mode title: "@kbn/cli-dev-mode" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/cli-dev-mode plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cli-dev-mode'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_coloring.mdx b/api_docs/kbn_coloring.mdx index 0f68cb49dec35..1c1853a813f82 100644 --- a/api_docs/kbn_coloring.mdx +++ b/api_docs/kbn_coloring.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-coloring title: "@kbn/coloring" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/coloring plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/coloring'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_config.mdx b/api_docs/kbn_config.mdx index d33fb0ca31e26..de9304b37fa96 100644 --- a/api_docs/kbn_config.mdx +++ b/api_docs/kbn_config.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-config title: "@kbn/config" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/config plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_config_mocks.mdx b/api_docs/kbn_config_mocks.mdx index 4235898a4f5b2..43479e7f25abb 100644 --- a/api_docs/kbn_config_mocks.mdx +++ b/api_docs/kbn_config_mocks.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-config-mocks title: "@kbn/config-mocks" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/config-mocks plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-mocks'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_config_schema.mdx b/api_docs/kbn_config_schema.mdx index 30f1f8636bdd8..dd452f8e7f0ba 100644 --- a/api_docs/kbn_config_schema.mdx +++ b/api_docs/kbn_config_schema.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-config-schema title: "@kbn/config-schema" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/config-schema plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-schema'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_analytics_browser.mdx b/api_docs/kbn_core_analytics_browser.mdx index 9d63767df02cb..34ca1ad7c523a 100644 --- a/api_docs/kbn_core_analytics_browser.mdx +++ b/api_docs/kbn_core_analytics_browser.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser title: "@kbn/core-analytics-browser" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-analytics-browser plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_analytics_browser_internal.mdx b/api_docs/kbn_core_analytics_browser_internal.mdx index 7415fdfd045cd..415c5c8dbef4e 100644 --- a/api_docs/kbn_core_analytics_browser_internal.mdx +++ b/api_docs/kbn_core_analytics_browser_internal.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-internal title: "@kbn/core-analytics-browser-internal" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-analytics-browser-internal plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-internal'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_analytics_browser_mocks.mdx b/api_docs/kbn_core_analytics_browser_mocks.mdx index 21657e30fdf16..06a3419da43d1 100644 --- a/api_docs/kbn_core_analytics_browser_mocks.mdx +++ b/api_docs/kbn_core_analytics_browser_mocks.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-mocks title: "@kbn/core-analytics-browser-mocks" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-analytics-browser-mocks plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-mocks'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_analytics_server.devdocs.json b/api_docs/kbn_core_analytics_server.devdocs.json new file mode 100644 index 0000000000000..d2f49935b878d --- /dev/null +++ b/api_docs/kbn_core_analytics_server.devdocs.json @@ -0,0 +1,114 @@ +{ + "id": "@kbn/core-analytics-server", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [ + { + "parentPluginId": "@kbn/core-analytics-server", + "id": "def-server.AnalyticsServicePreboot", + "type": "Type", + "tags": [], + "label": "AnalyticsServicePreboot", + "description": [ + "\nExposes the public APIs of the AnalyticsClient during the preboot phase\n{@link AnalyticsClient}" + ], + "signature": [ + "{ optIn: (optInConfig: ", + "OptInConfig", + ") => void; reportEvent: >(eventType: string, eventData: EventTypeData) => void; readonly telemetryCounter$: ", + "Observable", + "<", + "TelemetryCounter", + ">; registerEventType: (eventTypeOps: ", + "EventTypeOpts", + ") => void; registerShipper: (Shipper: ", + "ShipperClassConstructor", + ", shipperConfig: ShipperConfig, opts?: ", + "RegisterShipperOpts", + " | undefined) => void; registerContextProvider: (contextProviderOpts: ", + "ContextProviderOpts", + ") => void; removeContextProvider: (contextProviderName: string) => void; }" + ], + "path": "packages/core/analytics/core-analytics-server/src/contracts.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-analytics-server", + "id": "def-server.AnalyticsServiceSetup", + "type": "Type", + "tags": [], + "label": "AnalyticsServiceSetup", + "description": [ + "\nExposes the public APIs of the AnalyticsClient during the setup phase.\n{@link AnalyticsClient}" + ], + "signature": [ + "{ optIn: (optInConfig: ", + "OptInConfig", + ") => void; reportEvent: >(eventType: string, eventData: EventTypeData) => void; readonly telemetryCounter$: ", + "Observable", + "<", + "TelemetryCounter", + ">; registerEventType: (eventTypeOps: ", + "EventTypeOpts", + ") => void; registerShipper: (Shipper: ", + "ShipperClassConstructor", + ", shipperConfig: ShipperConfig, opts?: ", + "RegisterShipperOpts", + " | undefined) => void; registerContextProvider: (contextProviderOpts: ", + "ContextProviderOpts", + ") => void; removeContextProvider: (contextProviderName: string) => void; }" + ], + "path": "packages/core/analytics/core-analytics-server/src/contracts.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-analytics-server", + "id": "def-server.AnalyticsServiceStart", + "type": "Type", + "tags": [], + "label": "AnalyticsServiceStart", + "description": [ + "\nExposes the public APIs of the AnalyticsClient during the start phase\n{@link AnalyticsClient}" + ], + "signature": [ + "{ optIn: (optInConfig: ", + "OptInConfig", + ") => void; reportEvent: >(eventType: string, eventData: EventTypeData) => void; readonly telemetryCounter$: ", + "Observable", + "<", + "TelemetryCounter", + ">; }" + ], + "path": "packages/core/analytics/core-analytics-server/src/contracts.ts", + "deprecated": false, + "initialIsOpen": false + } + ], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_core_analytics_server.mdx b/api_docs/kbn_core_analytics_server.mdx new file mode 100644 index 0000000000000..453ba67379d04 --- /dev/null +++ b/api_docs/kbn_core_analytics_server.mdx @@ -0,0 +1,27 @@ +--- +id: kibKbnCoreAnalyticsServerPluginApi +slug: /kibana-dev-docs/api/kbn-core-analytics-server +title: "@kbn/core-analytics-server" +image: https://source.unsplash.com/400x175/?github +summary: API docs for the @kbn/core-analytics-server plugin +date: 2022-06-23 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server'] +warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. +--- +import kbnCoreAnalyticsServerObj from './kbn_core_analytics_server.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 3 | 0 | 0 | 0 | + +## Server + +### Consts, variables and types + + diff --git a/api_docs/kbn_core_analytics_server_internal.devdocs.json b/api_docs/kbn_core_analytics_server_internal.devdocs.json new file mode 100644 index 0000000000000..fda46f67f4a16 --- /dev/null +++ b/api_docs/kbn_core_analytics_server_internal.devdocs.json @@ -0,0 +1,134 @@ +{ + "id": "@kbn/core-analytics-server-internal", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [ + { + "parentPluginId": "@kbn/core-analytics-server-internal", + "id": "def-server.AnalyticsService", + "type": "Class", + "tags": [], + "label": "AnalyticsService", + "description": [], + "path": "packages/core/analytics/core-analytics-server-internal/src/analytics_service.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/core-analytics-server-internal", + "id": "def-server.AnalyticsService.Unnamed", + "type": "Function", + "tags": [], + "label": "Constructor", + "description": [], + "signature": [ + "any" + ], + "path": "packages/core/analytics/core-analytics-server-internal/src/analytics_service.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/core-analytics-server-internal", + "id": "def-server.AnalyticsService.Unnamed.$1", + "type": "Object", + "tags": [], + "label": "core", + "description": [], + "signature": [ + "CoreContext" + ], + "path": "packages/core/analytics/core-analytics-server-internal/src/analytics_service.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/core-analytics-server-internal", + "id": "def-server.AnalyticsService.preboot", + "type": "Function", + "tags": [], + "label": "preboot", + "description": [], + "signature": [ + "() => ", + "AnalyticsServicePreboot" + ], + "path": "packages/core/analytics/core-analytics-server-internal/src/analytics_service.ts", + "deprecated": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/core-analytics-server-internal", + "id": "def-server.AnalyticsService.setup", + "type": "Function", + "tags": [], + "label": "setup", + "description": [], + "signature": [ + "() => ", + "AnalyticsServiceSetup" + ], + "path": "packages/core/analytics/core-analytics-server-internal/src/analytics_service.ts", + "deprecated": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/core-analytics-server-internal", + "id": "def-server.AnalyticsService.start", + "type": "Function", + "tags": [], + "label": "start", + "description": [], + "signature": [ + "() => ", + "AnalyticsServiceStart" + ], + "path": "packages/core/analytics/core-analytics-server-internal/src/analytics_service.ts", + "deprecated": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/core-analytics-server-internal", + "id": "def-server.AnalyticsService.stop", + "type": "Function", + "tags": [], + "label": "stop", + "description": [], + "signature": [ + "() => void" + ], + "path": "packages/core/analytics/core-analytics-server-internal/src/analytics_service.ts", + "deprecated": false, + "children": [], + "returnComment": [] + } + ], + "initialIsOpen": false + } + ], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_core_analytics_server_internal.mdx b/api_docs/kbn_core_analytics_server_internal.mdx new file mode 100644 index 0000000000000..b033e3316a624 --- /dev/null +++ b/api_docs/kbn_core_analytics_server_internal.mdx @@ -0,0 +1,27 @@ +--- +id: kibKbnCoreAnalyticsServerInternalPluginApi +slug: /kibana-dev-docs/api/kbn-core-analytics-server-internal +title: "@kbn/core-analytics-server-internal" +image: https://source.unsplash.com/400x175/?github +summary: API docs for the @kbn/core-analytics-server-internal plugin +date: 2022-06-23 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-internal'] +warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. +--- +import kbnCoreAnalyticsServerInternalObj from './kbn_core_analytics_server_internal.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 7 | 0 | 7 | 0 | + +## Server + +### Classes + + diff --git a/api_docs/kbn_core_analytics_server_mocks.devdocs.json b/api_docs/kbn_core_analytics_server_mocks.devdocs.json new file mode 100644 index 0000000000000..6d6fe2b36e0e5 --- /dev/null +++ b/api_docs/kbn_core_analytics_server_mocks.devdocs.json @@ -0,0 +1,107 @@ +{ + "id": "@kbn/core-analytics-server-mocks", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [ + { + "parentPluginId": "@kbn/core-analytics-server-mocks", + "id": "def-server.analyticsServiceMock", + "type": "Object", + "tags": [], + "label": "analyticsServiceMock", + "description": [], + "path": "packages/core/analytics/core-analytics-server-mocks/src/analytics_service.mock.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/core-analytics-server-mocks", + "id": "def-server.analyticsServiceMock.create", + "type": "Function", + "tags": [], + "label": "create", + "description": [], + "signature": [ + "() => jest.Mocked" + ], + "path": "packages/core/analytics/core-analytics-server-mocks/src/analytics_service.mock.ts", + "deprecated": false, + "returnComment": [], + "children": [] + }, + { + "parentPluginId": "@kbn/core-analytics-server-mocks", + "id": "def-server.analyticsServiceMock.createAnalyticsServicePreboot", + "type": "Function", + "tags": [], + "label": "createAnalyticsServicePreboot", + "description": [], + "signature": [ + "() => jest.Mocked<", + "AnalyticsServicePreboot", + ">" + ], + "path": "packages/core/analytics/core-analytics-server-mocks/src/analytics_service.mock.ts", + "deprecated": false, + "returnComment": [], + "children": [] + }, + { + "parentPluginId": "@kbn/core-analytics-server-mocks", + "id": "def-server.analyticsServiceMock.createAnalyticsServiceSetup", + "type": "Function", + "tags": [], + "label": "createAnalyticsServiceSetup", + "description": [], + "signature": [ + "() => jest.Mocked<", + "AnalyticsServiceSetup", + ">" + ], + "path": "packages/core/analytics/core-analytics-server-mocks/src/analytics_service.mock.ts", + "deprecated": false, + "returnComment": [], + "children": [] + }, + { + "parentPluginId": "@kbn/core-analytics-server-mocks", + "id": "def-server.analyticsServiceMock.createAnalyticsServiceStart", + "type": "Function", + "tags": [], + "label": "createAnalyticsServiceStart", + "description": [], + "signature": [ + "() => jest.Mocked<", + "AnalyticsServiceStart", + ">" + ], + "path": "packages/core/analytics/core-analytics-server-mocks/src/analytics_service.mock.ts", + "deprecated": false, + "returnComment": [], + "children": [] + } + ], + "initialIsOpen": false + } + ] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_core_analytics_server_mocks.mdx b/api_docs/kbn_core_analytics_server_mocks.mdx new file mode 100644 index 0000000000000..8df7dd430ee41 --- /dev/null +++ b/api_docs/kbn_core_analytics_server_mocks.mdx @@ -0,0 +1,27 @@ +--- +id: kibKbnCoreAnalyticsServerMocksPluginApi +slug: /kibana-dev-docs/api/kbn-core-analytics-server-mocks +title: "@kbn/core-analytics-server-mocks" +image: https://source.unsplash.com/400x175/?github +summary: API docs for the @kbn/core-analytics-server-mocks plugin +date: 2022-06-23 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-mocks'] +warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. +--- +import kbnCoreAnalyticsServerMocksObj from './kbn_core_analytics_server_mocks.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 5 | 0 | 5 | 0 | + +## Server + +### Objects + + diff --git a/api_docs/kbn_core_base_browser_mocks.mdx b/api_docs/kbn_core_base_browser_mocks.mdx index 64f89ba035988..1d672c961f10d 100644 --- a/api_docs/kbn_core_base_browser_mocks.mdx +++ b/api_docs/kbn_core_base_browser_mocks.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-browser-mocks title: "@kbn/core-base-browser-mocks" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-base-browser-mocks plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-browser-mocks'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_base_common.mdx b/api_docs/kbn_core_base_common.mdx index 8f7fc1a42003c..e5e75b54734ce 100644 --- a/api_docs/kbn_core_base_common.mdx +++ b/api_docs/kbn_core_base_common.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-common title: "@kbn/core-base-common" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-base-common plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-common'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_base_server_internal.devdocs.json b/api_docs/kbn_core_base_server_internal.devdocs.json new file mode 100644 index 0000000000000..6cc5a5d30adc9 --- /dev/null +++ b/api_docs/kbn_core_base_server_internal.devdocs.json @@ -0,0 +1,123 @@ +{ + "id": "@kbn/core-base-server-internal", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [ + { + "parentPluginId": "@kbn/core-base-server-internal", + "id": "def-server.CriticalError", + "type": "Class", + "tags": [], + "label": "CriticalError", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-base-server-internal", + "scope": "server", + "docId": "kibKbnCoreBaseServerInternalPluginApi", + "section": "def-server.CriticalError", + "text": "CriticalError" + }, + " extends Error" + ], + "path": "packages/core/base/core-base-server-internal/src/errors.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/core-base-server-internal", + "id": "def-server.CriticalError.Unnamed", + "type": "Function", + "tags": [], + "label": "Constructor", + "description": [], + "signature": [ + "any" + ], + "path": "packages/core/base/core-base-server-internal/src/errors.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/core-base-server-internal", + "id": "def-server.CriticalError.Unnamed.$1", + "type": "string", + "tags": [], + "label": "message", + "description": [], + "signature": [ + "string" + ], + "path": "packages/core/base/core-base-server-internal/src/errors.ts", + "deprecated": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/core-base-server-internal", + "id": "def-server.CriticalError.Unnamed.$2", + "type": "string", + "tags": [], + "label": "code", + "description": [], + "signature": [ + "string" + ], + "path": "packages/core/base/core-base-server-internal/src/errors.ts", + "deprecated": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/core-base-server-internal", + "id": "def-server.CriticalError.Unnamed.$3", + "type": "number", + "tags": [], + "label": "processExitCode", + "description": [], + "signature": [ + "number" + ], + "path": "packages/core/base/core-base-server-internal/src/errors.ts", + "deprecated": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/core-base-server-internal", + "id": "def-server.CriticalError.Unnamed.$4", + "type": "Object", + "tags": [], + "label": "cause", + "description": [], + "signature": [ + "Error | undefined" + ], + "path": "packages/core/base/core-base-server-internal/src/errors.ts", + "deprecated": false, + "isRequired": false + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false + } + ], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_core_base_server_internal.mdx b/api_docs/kbn_core_base_server_internal.mdx new file mode 100644 index 0000000000000..86cb682ec482e --- /dev/null +++ b/api_docs/kbn_core_base_server_internal.mdx @@ -0,0 +1,27 @@ +--- +id: kibKbnCoreBaseServerInternalPluginApi +slug: /kibana-dev-docs/api/kbn-core-base-server-internal +title: "@kbn/core-base-server-internal" +image: https://source.unsplash.com/400x175/?github +summary: API docs for the @kbn/core-base-server-internal plugin +date: 2022-06-23 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-internal'] +warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. +--- +import kbnCoreBaseServerInternalObj from './kbn_core_base_server_internal.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 6 | 0 | 6 | 0 | + +## Server + +### Classes + + diff --git a/api_docs/kbn_core_base_server_mocks.mdx b/api_docs/kbn_core_base_server_mocks.mdx index be4d31b7e6153..5a9657f828551 100644 --- a/api_docs/kbn_core_base_server_mocks.mdx +++ b/api_docs/kbn_core_base_server_mocks.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-mocks title: "@kbn/core-base-server-mocks" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-base-server-mocks plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-mocks'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_config_server_internal.devdocs.json b/api_docs/kbn_core_config_server_internal.devdocs.json new file mode 100644 index 0000000000000..da4e89e53775a --- /dev/null +++ b/api_docs/kbn_core_config_server_internal.devdocs.json @@ -0,0 +1,260 @@ +{ + "id": "@kbn/core-config-server-internal", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [ + { + "parentPluginId": "@kbn/core-config-server-internal", + "id": "def-server.coreDeprecationProvider", + "type": "Function", + "tags": [], + "label": "coreDeprecationProvider", + "description": [], + "signature": [ + "() => ", + "ConfigDeprecation", + "[]" + ], + "path": "packages/core/config/core-config-server-internal/src/deprecation/core_deprecations.ts", + "deprecated": false, + "children": [], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-config-server-internal", + "id": "def-server.ensureValidConfiguration", + "type": "Function", + "tags": [], + "label": "ensureValidConfiguration", + "description": [], + "signature": [ + "(configService: ", + "ConfigService", + ", params: ", + "ConfigValidateParameters", + " | undefined) => Promise" + ], + "path": "packages/core/config/core-config-server-internal/src/ensure_valid_configuration.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/core-config-server-internal", + "id": "def-server.ensureValidConfiguration.$1", + "type": "Object", + "tags": [], + "label": "configService", + "description": [], + "signature": [ + "ConfigService" + ], + "path": "packages/core/config/core-config-server-internal/src/ensure_valid_configuration.ts", + "deprecated": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/core-config-server-internal", + "id": "def-server.ensureValidConfiguration.$2", + "type": "Object", + "tags": [], + "label": "params", + "description": [], + "signature": [ + "ConfigValidateParameters", + " | undefined" + ], + "path": "packages/core/config/core-config-server-internal/src/ensure_valid_configuration.ts", + "deprecated": false, + "isRequired": false + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-config-server-internal", + "id": "def-server.getDeprecationsFor", + "type": "Function", + "tags": [], + "label": "getDeprecationsFor", + "description": [], + "signature": [ + "({ provider, settings, path, }: { provider: ", + "ConfigDeprecationProvider", + "; settings?: Record | undefined; path: string; }) => { messages: string[]; levels: string[]; migrated: Record; }" + ], + "path": "packages/core/config/core-config-server-internal/src/test_utils.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/core-config-server-internal", + "id": "def-server.getDeprecationsFor.$1", + "type": "Object", + "tags": [], + "label": "{\n provider,\n settings = {},\n path,\n}", + "description": [], + "path": "packages/core/config/core-config-server-internal/src/test_utils.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/core-config-server-internal", + "id": "def-server.getDeprecationsFor.$1.provider", + "type": "Function", + "tags": [], + "label": "provider", + "description": [], + "signature": [ + "(factory: ", + "ConfigDeprecationFactory", + ") => ", + "ConfigDeprecation", + "[]" + ], + "path": "packages/core/config/core-config-server-internal/src/test_utils.ts", + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/core-config-server-internal", + "id": "def-server.getDeprecationsFor.$1.provider.$1", + "type": "Object", + "tags": [], + "label": "factory", + "description": [], + "signature": [ + "ConfigDeprecationFactory" + ], + "path": "node_modules/@types/kbn__config/index.d.ts", + "deprecated": false + } + ] + }, + { + "parentPluginId": "@kbn/core-config-server-internal", + "id": "def-server.getDeprecationsFor.$1.settings", + "type": "Object", + "tags": [], + "label": "settings", + "description": [], + "signature": [ + "Record | undefined" + ], + "path": "packages/core/config/core-config-server-internal/src/test_utils.ts", + "deprecated": false + }, + { + "parentPluginId": "@kbn/core-config-server-internal", + "id": "def-server.getDeprecationsFor.$1.path", + "type": "string", + "tags": [], + "label": "path", + "description": [], + "path": "packages/core/config/core-config-server-internal/src/test_utils.ts", + "deprecated": false + } + ] + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-config-server-internal", + "id": "def-server.getDeprecationsForGlobalSettings", + "type": "Function", + "tags": [], + "label": "getDeprecationsForGlobalSettings", + "description": [], + "signature": [ + "({ provider, settings, }: { provider: ", + "ConfigDeprecationProvider", + "; settings?: Record | undefined; }) => { messages: string[]; levels: string[]; migrated: Record; }" + ], + "path": "packages/core/config/core-config-server-internal/src/test_utils.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/core-config-server-internal", + "id": "def-server.getDeprecationsForGlobalSettings.$1", + "type": "Object", + "tags": [], + "label": "{\n provider,\n settings = {},\n}", + "description": [], + "path": "packages/core/config/core-config-server-internal/src/test_utils.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/core-config-server-internal", + "id": "def-server.getDeprecationsForGlobalSettings.$1.provider", + "type": "Function", + "tags": [], + "label": "provider", + "description": [], + "signature": [ + "(factory: ", + "ConfigDeprecationFactory", + ") => ", + "ConfigDeprecation", + "[]" + ], + "path": "packages/core/config/core-config-server-internal/src/test_utils.ts", + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/core-config-server-internal", + "id": "def-server.getDeprecationsForGlobalSettings.$1.provider.$1", + "type": "Object", + "tags": [], + "label": "factory", + "description": [], + "signature": [ + "ConfigDeprecationFactory" + ], + "path": "node_modules/@types/kbn__config/index.d.ts", + "deprecated": false + } + ] + }, + { + "parentPluginId": "@kbn/core-config-server-internal", + "id": "def-server.getDeprecationsForGlobalSettings.$1.settings", + "type": "Object", + "tags": [], + "label": "settings", + "description": [], + "signature": [ + "Record | undefined" + ], + "path": "packages/core/config/core-config-server-internal/src/test_utils.ts", + "deprecated": false + } + ] + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_core_config_server_internal.mdx b/api_docs/kbn_core_config_server_internal.mdx new file mode 100644 index 0000000000000..b6cbf0d678888 --- /dev/null +++ b/api_docs/kbn_core_config_server_internal.mdx @@ -0,0 +1,27 @@ +--- +id: kibKbnCoreConfigServerInternalPluginApi +slug: /kibana-dev-docs/api/kbn-core-config-server-internal +title: "@kbn/core-config-server-internal" +image: https://source.unsplash.com/400x175/?github +summary: API docs for the @kbn/core-config-server-internal plugin +date: 2022-06-23 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-config-server-internal'] +warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. +--- +import kbnCoreConfigServerInternalObj from './kbn_core_config_server_internal.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 15 | 0 | 13 | 0 | + +## Server + +### Functions + + diff --git a/api_docs/kbn_core_doc_links_browser.mdx b/api_docs/kbn_core_doc_links_browser.mdx index cf5302dd72f77..619374b571b77 100644 --- a/api_docs/kbn_core_doc_links_browser.mdx +++ b/api_docs/kbn_core_doc_links_browser.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser title: "@kbn/core-doc-links-browser" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-doc-links-browser plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_doc_links_browser_mocks.mdx b/api_docs/kbn_core_doc_links_browser_mocks.mdx index 0e821cd157098..4dd6b03d0161c 100644 --- a/api_docs/kbn_core_doc_links_browser_mocks.mdx +++ b/api_docs/kbn_core_doc_links_browser_mocks.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser-mocks title: "@kbn/core-doc-links-browser-mocks" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-doc-links-browser-mocks plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser-mocks'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_doc_links_server.mdx b/api_docs/kbn_core_doc_links_server.mdx index 3b3ca4bcaa5a3..b5343541902fb 100644 --- a/api_docs/kbn_core_doc_links_server.mdx +++ b/api_docs/kbn_core_doc_links_server.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server title: "@kbn/core-doc-links-server" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-doc-links-server plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_doc_links_server_mocks.mdx b/api_docs/kbn_core_doc_links_server_mocks.mdx index 76c157aa5e054..d9d3756d07cbd 100644 --- a/api_docs/kbn_core_doc_links_server_mocks.mdx +++ b/api_docs/kbn_core_doc_links_server_mocks.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server-mocks title: "@kbn/core-doc-links-server-mocks" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-doc-links-server-mocks plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server-mocks'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_i18n_browser.devdocs.json b/api_docs/kbn_core_i18n_browser.devdocs.json new file mode 100644 index 0000000000000..28c1fd36c3064 --- /dev/null +++ b/api_docs/kbn_core_i18n_browser.devdocs.json @@ -0,0 +1,86 @@ +{ + "id": "@kbn/core-i18n-browser", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [ + { + "parentPluginId": "@kbn/core-i18n-browser", + "id": "def-common.I18nStart", + "type": "Interface", + "tags": [], + "label": "I18nStart", + "description": [ + "\nI18nStart.Context is required by any localizable React component from \\@kbn/i18n and \\@elastic/eui packages\nand is supposed to be used as the topmost component for any i18n-compatible React tree.\n" + ], + "path": "packages/core/i18n/core-i18n-browser/src/types.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/core-i18n-browser", + "id": "def-common.I18nStart.Context", + "type": "Function", + "tags": [], + "label": "Context", + "description": [ + "\nReact Context provider required as the topmost component for any i18n-compatible React tree." + ], + "signature": [ + "({ children }: { children: React.ReactNode; }) => JSX.Element" + ], + "path": "packages/core/i18n/core-i18n-browser/src/types.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/core-i18n-browser", + "id": "def-common.I18nStart.Context.$1", + "type": "Object", + "tags": [], + "label": "{ children }", + "description": [], + "path": "packages/core/i18n/core-i18n-browser/src/types.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/core-i18n-browser", + "id": "def-common.I18nStart.Context.$1.children", + "type": "CompoundType", + "tags": [], + "label": "children", + "description": [], + "signature": [ + "boolean | React.ReactChild | React.ReactFragment | React.ReactPortal | null | undefined" + ], + "path": "packages/core/i18n/core-i18n-browser/src/types.ts", + "deprecated": false + } + ] + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false + } + ], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_core_i18n_browser.mdx b/api_docs/kbn_core_i18n_browser.mdx new file mode 100644 index 0000000000000..fe2b0abb4b4cc --- /dev/null +++ b/api_docs/kbn_core_i18n_browser.mdx @@ -0,0 +1,27 @@ +--- +id: kibKbnCoreI18nBrowserPluginApi +slug: /kibana-dev-docs/api/kbn-core-i18n-browser +title: "@kbn/core-i18n-browser" +image: https://source.unsplash.com/400x175/?github +summary: API docs for the @kbn/core-i18n-browser plugin +date: 2022-06-23 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser'] +warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. +--- +import kbnCoreI18nBrowserObj from './kbn_core_i18n_browser.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 4 | 0 | 2 | 0 | + +## Common + +### Interfaces + + diff --git a/api_docs/kbn_core_i18n_browser_mocks.devdocs.json b/api_docs/kbn_core_i18n_browser_mocks.devdocs.json new file mode 100644 index 0000000000000..7811c288caed5 --- /dev/null +++ b/api_docs/kbn_core_i18n_browser_mocks.devdocs.json @@ -0,0 +1,73 @@ +{ + "id": "@kbn/core-i18n-browser-mocks", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [ + { + "parentPluginId": "@kbn/core-i18n-browser-mocks", + "id": "def-common.i18nServiceMock", + "type": "Object", + "tags": [], + "label": "i18nServiceMock", + "description": [], + "path": "packages/core/i18n/core-i18n-browser-mocks/src/i18n_service.mock.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/core-i18n-browser-mocks", + "id": "def-common.i18nServiceMock.create", + "type": "Function", + "tags": [], + "label": "create", + "description": [], + "signature": [ + "() => jest.Mocked" + ], + "path": "packages/core/i18n/core-i18n-browser-mocks/src/i18n_service.mock.ts", + "deprecated": false, + "returnComment": [], + "children": [] + }, + { + "parentPluginId": "@kbn/core-i18n-browser-mocks", + "id": "def-common.i18nServiceMock.createStartContract", + "type": "Function", + "tags": [], + "label": "createStartContract", + "description": [], + "signature": [ + "() => jest.Mocked<", + "I18nStart", + ">" + ], + "path": "packages/core/i18n/core-i18n-browser-mocks/src/i18n_service.mock.ts", + "deprecated": false, + "returnComment": [], + "children": [] + } + ], + "initialIsOpen": false + } + ] + } +} \ No newline at end of file diff --git a/api_docs/kbn_core_i18n_browser_mocks.mdx b/api_docs/kbn_core_i18n_browser_mocks.mdx new file mode 100644 index 0000000000000..715b38c4aef94 --- /dev/null +++ b/api_docs/kbn_core_i18n_browser_mocks.mdx @@ -0,0 +1,27 @@ +--- +id: kibKbnCoreI18nBrowserMocksPluginApi +slug: /kibana-dev-docs/api/kbn-core-i18n-browser-mocks +title: "@kbn/core-i18n-browser-mocks" +image: https://source.unsplash.com/400x175/?github +summary: API docs for the @kbn/core-i18n-browser-mocks plugin +date: 2022-06-23 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser-mocks'] +warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. +--- +import kbnCoreI18nBrowserMocksObj from './kbn_core_i18n_browser_mocks.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 3 | 0 | 3 | 0 | + +## Common + +### Objects + + diff --git a/api_docs/kbn_core_injected_metadata_browser.mdx b/api_docs/kbn_core_injected_metadata_browser.mdx index b3816f2d31255..0dfa95e8653ac 100644 --- a/api_docs/kbn_core_injected_metadata_browser.mdx +++ b/api_docs/kbn_core_injected_metadata_browser.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-injected-metadata-browser title: "@kbn/core-injected-metadata-browser" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-injected-metadata-browser plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-injected-metadata-browser'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx index e5fde4fca46e0..18dd111b5bdea 100644 --- a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx +++ b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-injected-metadata-browser-mocks title: "@kbn/core-injected-metadata-browser-mocks" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-injected-metadata-browser-mocks plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-injected-metadata-browser-mocks'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_logging_server.mdx b/api_docs/kbn_core_logging_server.mdx index 4bd8571aa5858..b7d51a892cad4 100644 --- a/api_docs/kbn_core_logging_server.mdx +++ b/api_docs/kbn_core_logging_server.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server title: "@kbn/core-logging-server" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-logging-server plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_logging_server_internal.mdx b/api_docs/kbn_core_logging_server_internal.mdx index 452fb9a6deda9..24787602fd826 100644 --- a/api_docs/kbn_core_logging_server_internal.mdx +++ b/api_docs/kbn_core_logging_server_internal.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-internal title: "@kbn/core-logging-server-internal" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-logging-server-internal plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-internal'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_logging_server_mocks.mdx b/api_docs/kbn_core_logging_server_mocks.mdx index 93254645d2cc1..596ae2e2d0c87 100644 --- a/api_docs/kbn_core_logging_server_mocks.mdx +++ b/api_docs/kbn_core_logging_server_mocks.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-mocks title: "@kbn/core-logging-server-mocks" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-logging-server-mocks plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-mocks'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_theme_browser.mdx b/api_docs/kbn_core_theme_browser.mdx index 50ace4e85197f..73ec2992c6056 100644 --- a/api_docs/kbn_core_theme_browser.mdx +++ b/api_docs/kbn_core_theme_browser.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser title: "@kbn/core-theme-browser" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-theme-browser plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_theme_browser_mocks.mdx b/api_docs/kbn_core_theme_browser_mocks.mdx index 1d7f02a176b3e..fa6b098cf25ac 100644 --- a/api_docs/kbn_core_theme_browser_mocks.mdx +++ b/api_docs/kbn_core_theme_browser_mocks.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser-mocks title: "@kbn/core-theme-browser-mocks" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-theme-browser-mocks plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser-mocks'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_crypto.mdx b/api_docs/kbn_crypto.mdx index bd979a55a7b96..05421434cb5b9 100644 --- a/api_docs/kbn_crypto.mdx +++ b/api_docs/kbn_crypto.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-crypto title: "@kbn/crypto" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/crypto plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_datemath.mdx b/api_docs/kbn_datemath.mdx index 2873ceafac55f..3507ad5af0a3f 100644 --- a/api_docs/kbn_datemath.mdx +++ b/api_docs/kbn_datemath.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-datemath title: "@kbn/datemath" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/datemath plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/datemath'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_dev_cli_errors.mdx b/api_docs/kbn_dev_cli_errors.mdx index a4686de799c9d..c7fc9e821a93f 100644 --- a/api_docs/kbn_dev_cli_errors.mdx +++ b/api_docs/kbn_dev_cli_errors.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-errors title: "@kbn/dev-cli-errors" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/dev-cli-errors plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-errors'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_dev_cli_runner.mdx b/api_docs/kbn_dev_cli_runner.mdx index 9247656496398..ccf27f5dc4907 100644 --- a/api_docs/kbn_dev_cli_runner.mdx +++ b/api_docs/kbn_dev_cli_runner.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-runner title: "@kbn/dev-cli-runner" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/dev-cli-runner plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-runner'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_dev_proc_runner.mdx b/api_docs/kbn_dev_proc_runner.mdx index 86b45e3b28215..613bddfbc0e6a 100644 --- a/api_docs/kbn_dev_proc_runner.mdx +++ b/api_docs/kbn_dev_proc_runner.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-dev-proc-runner title: "@kbn/dev-proc-runner" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/dev-proc-runner plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-proc-runner'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_dev_utils.mdx b/api_docs/kbn_dev_utils.mdx index 3fa3295fe605d..80ede3a8f2ce5 100644 --- a/api_docs/kbn_dev_utils.mdx +++ b/api_docs/kbn_dev_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-dev-utils title: "@kbn/dev-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/dev-utils plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_doc_links.mdx b/api_docs/kbn_doc_links.mdx index abb751f0b8964..f35f2a788324c 100644 --- a/api_docs/kbn_doc_links.mdx +++ b/api_docs/kbn_doc_links.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-doc-links title: "@kbn/doc-links" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/doc-links plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/doc-links'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_docs_utils.mdx b/api_docs/kbn_docs_utils.mdx index c1d0ef70fb894..5425bf061fc86 100644 --- a/api_docs/kbn_docs_utils.mdx +++ b/api_docs/kbn_docs_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-docs-utils title: "@kbn/docs-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/docs-utils plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/docs-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_es_archiver.mdx b/api_docs/kbn_es_archiver.mdx index a65bbce6b7df5..fea0955a22e95 100644 --- a/api_docs/kbn_es_archiver.mdx +++ b/api_docs/kbn_es_archiver.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-es-archiver title: "@kbn/es-archiver" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/es-archiver plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-archiver'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_es_query.mdx b/api_docs/kbn_es_query.mdx index c71f7ac7ce64c..a6869f197252d 100644 --- a/api_docs/kbn_es_query.mdx +++ b/api_docs/kbn_es_query.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-es-query title: "@kbn/es-query" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/es-query plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-query'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_eslint_plugin_imports.mdx b/api_docs/kbn_eslint_plugin_imports.mdx index 79cedb14c81e1..9bdfd637706b9 100644 --- a/api_docs/kbn_eslint_plugin_imports.mdx +++ b/api_docs/kbn_eslint_plugin_imports.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-eslint-plugin-imports title: "@kbn/eslint-plugin-imports" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/eslint-plugin-imports plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/eslint-plugin-imports'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_field_types.mdx b/api_docs/kbn_field_types.mdx index 79219f14c9a80..fd68af1589767 100644 --- a/api_docs/kbn_field_types.mdx +++ b/api_docs/kbn_field_types.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-field-types title: "@kbn/field-types" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/field-types plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/field-types'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_find_used_node_modules.mdx b/api_docs/kbn_find_used_node_modules.mdx index d74690a25741d..9c2f9020578e2 100644 --- a/api_docs/kbn_find_used_node_modules.mdx +++ b/api_docs/kbn_find_used_node_modules.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-find-used-node-modules title: "@kbn/find-used-node-modules" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/find-used-node-modules plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/find-used-node-modules'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_generate.mdx b/api_docs/kbn_generate.mdx index d0a9254642187..4c8ed429da1c2 100644 --- a/api_docs/kbn_generate.mdx +++ b/api_docs/kbn_generate.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-generate title: "@kbn/generate" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/generate plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_handlebars.mdx b/api_docs/kbn_handlebars.mdx index ebb98181a03e3..321c9000cd1d7 100644 --- a/api_docs/kbn_handlebars.mdx +++ b/api_docs/kbn_handlebars.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-handlebars title: "@kbn/handlebars" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/handlebars plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/handlebars'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_i18n.mdx b/api_docs/kbn_i18n.mdx index 924a77a4dc73c..55782575d4a2d 100644 --- a/api_docs/kbn_i18n.mdx +++ b/api_docs/kbn_i18n.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-i18n title: "@kbn/i18n" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/i18n plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_import_resolver.mdx b/api_docs/kbn_import_resolver.mdx index 689395ca46f78..cfc5f243f43d7 100644 --- a/api_docs/kbn_import_resolver.mdx +++ b/api_docs/kbn_import_resolver.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-import-resolver title: "@kbn/import-resolver" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/import-resolver plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/import-resolver'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_interpreter.mdx b/api_docs/kbn_interpreter.mdx index 9f6abcfed25a9..4e73e2c5ace40 100644 --- a/api_docs/kbn_interpreter.mdx +++ b/api_docs/kbn_interpreter.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-interpreter title: "@kbn/interpreter" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/interpreter plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/interpreter'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_io_ts_utils.mdx b/api_docs/kbn_io_ts_utils.mdx index c85e1c5161d3a..ef11b76adc992 100644 --- a/api_docs/kbn_io_ts_utils.mdx +++ b/api_docs/kbn_io_ts_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-io-ts-utils title: "@kbn/io-ts-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/io-ts-utils plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/io-ts-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_jest_serializers.mdx b/api_docs/kbn_jest_serializers.mdx index 10d09ba774ead..1380af516f2d0 100644 --- a/api_docs/kbn_jest_serializers.mdx +++ b/api_docs/kbn_jest_serializers.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-jest-serializers title: "@kbn/jest-serializers" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/jest-serializers plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/jest-serializers'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_kibana_json_schema.mdx b/api_docs/kbn_kibana_json_schema.mdx index 1c27bd1550d86..a3d937b8ee946 100644 --- a/api_docs/kbn_kibana_json_schema.mdx +++ b/api_docs/kbn_kibana_json_schema.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-kibana-json-schema title: "@kbn/kibana-json-schema" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/kibana-json-schema plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/kibana-json-schema'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_logging.mdx b/api_docs/kbn_logging.mdx index a10e412bf4f01..54487887fbc56 100644 --- a/api_docs/kbn_logging.mdx +++ b/api_docs/kbn_logging.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-logging title: "@kbn/logging" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/logging plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_logging_mocks.mdx b/api_docs/kbn_logging_mocks.mdx index 143b29a9f865d..b4188f4410c64 100644 --- a/api_docs/kbn_logging_mocks.mdx +++ b/api_docs/kbn_logging_mocks.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-logging-mocks title: "@kbn/logging-mocks" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/logging-mocks plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging-mocks'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_mapbox_gl.mdx b/api_docs/kbn_mapbox_gl.mdx index fa1f9f2c6ec41..d031ab2e997bd 100644 --- a/api_docs/kbn_mapbox_gl.mdx +++ b/api_docs/kbn_mapbox_gl.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-mapbox-gl title: "@kbn/mapbox-gl" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/mapbox-gl plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/mapbox-gl'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_monaco.mdx b/api_docs/kbn_monaco.mdx index bc92c78f29686..339350026a676 100644 --- a/api_docs/kbn_monaco.mdx +++ b/api_docs/kbn_monaco.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-monaco title: "@kbn/monaco" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/monaco plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/monaco'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_optimizer.mdx b/api_docs/kbn_optimizer.mdx index bb27ae64cb91a..d6c3951da8a48 100644 --- a/api_docs/kbn_optimizer.mdx +++ b/api_docs/kbn_optimizer.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer title: "@kbn/optimizer" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/optimizer plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_optimizer_webpack_helpers.mdx b/api_docs/kbn_optimizer_webpack_helpers.mdx index e484f0ead7a58..73e3151761d74 100644 --- a/api_docs/kbn_optimizer_webpack_helpers.mdx +++ b/api_docs/kbn_optimizer_webpack_helpers.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer-webpack-helpers title: "@kbn/optimizer-webpack-helpers" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/optimizer-webpack-helpers plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer-webpack-helpers'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_performance_testing_dataset_extractor.mdx b/api_docs/kbn_performance_testing_dataset_extractor.mdx index 5359b6d6ca332..5be0ada3b19e4 100644 --- a/api_docs/kbn_performance_testing_dataset_extractor.mdx +++ b/api_docs/kbn_performance_testing_dataset_extractor.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-performance-testing-dataset-extractor title: "@kbn/performance-testing-dataset-extractor" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/performance-testing-dataset-extractor plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/performance-testing-dataset-extractor'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_plugin_discovery.mdx b/api_docs/kbn_plugin_discovery.mdx index 5a84642278865..90e958b824606 100644 --- a/api_docs/kbn_plugin_discovery.mdx +++ b/api_docs/kbn_plugin_discovery.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-discovery title: "@kbn/plugin-discovery" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/plugin-discovery plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-discovery'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_plugin_generator.mdx b/api_docs/kbn_plugin_generator.mdx index a134aaa626f0e..619141948aebe 100644 --- a/api_docs/kbn_plugin_generator.mdx +++ b/api_docs/kbn_plugin_generator.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-generator title: "@kbn/plugin-generator" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/plugin-generator plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-generator'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_plugin_helpers.mdx b/api_docs/kbn_plugin_helpers.mdx index 8c8c853b9d0ab..9030770483370 100644 --- a/api_docs/kbn_plugin_helpers.mdx +++ b/api_docs/kbn_plugin_helpers.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-helpers title: "@kbn/plugin-helpers" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/plugin-helpers plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-helpers'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_pm.mdx b/api_docs/kbn_pm.mdx index 8903529981b99..8ed75da929b79 100644 --- a/api_docs/kbn_pm.mdx +++ b/api_docs/kbn_pm.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-pm title: "@kbn/pm" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/pm plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/pm'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_react_field.mdx b/api_docs/kbn_react_field.mdx index 97eaa4abd5079..429c623648380 100644 --- a/api_docs/kbn_react_field.mdx +++ b/api_docs/kbn_react_field.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-react-field title: "@kbn/react-field" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/react-field plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-field'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_rule_data_utils.mdx b/api_docs/kbn_rule_data_utils.mdx index f5b53a4c08a99..9c72ccef64e2f 100644 --- a/api_docs/kbn_rule_data_utils.mdx +++ b/api_docs/kbn_rule_data_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-rule-data-utils title: "@kbn/rule-data-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/rule-data-utils plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rule-data-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_scalability_simulation_generator.mdx b/api_docs/kbn_scalability_simulation_generator.mdx index 9a7b205f79ab6..0c9777f519007 100644 --- a/api_docs/kbn_scalability_simulation_generator.mdx +++ b/api_docs/kbn_scalability_simulation_generator.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-scalability-simulation-generator title: "@kbn/scalability-simulation-generator" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/scalability-simulation-generator plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/scalability-simulation-generator'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_autocomplete.mdx b/api_docs/kbn_securitysolution_autocomplete.mdx index d9159b8c1b379..6a6308e1ffae9 100644 --- a/api_docs/kbn_securitysolution_autocomplete.mdx +++ b/api_docs/kbn_securitysolution_autocomplete.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-autocomplete title: "@kbn/securitysolution-autocomplete" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-autocomplete plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-autocomplete'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_es_utils.mdx b/api_docs/kbn_securitysolution_es_utils.mdx index 944f24b63a6cc..2ee30c0656dcf 100644 --- a/api_docs/kbn_securitysolution_es_utils.mdx +++ b/api_docs/kbn_securitysolution_es_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-es-utils title: "@kbn/securitysolution-es-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-es-utils plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-es-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_hook_utils.mdx b/api_docs/kbn_securitysolution_hook_utils.mdx index d8a77eaeeb160..dcc8bf31d9049 100644 --- a/api_docs/kbn_securitysolution_hook_utils.mdx +++ b/api_docs/kbn_securitysolution_hook_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-hook-utils title: "@kbn/securitysolution-hook-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-hook-utils plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-hook-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx index 1c3ec9f3c5fa4..117a1eb2c5159 100644 --- a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-alerting-types title: "@kbn/securitysolution-io-ts-alerting-types" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-io-ts-alerting-types plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-alerting-types'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_io_ts_list_types.mdx b/api_docs/kbn_securitysolution_io_ts_list_types.mdx index 0535b4655d6dd..9c5752b203eb0 100644 --- a/api_docs/kbn_securitysolution_io_ts_list_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_list_types.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-list-types title: "@kbn/securitysolution-io-ts-list-types" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-io-ts-list-types plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-list-types'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_io_ts_types.mdx b/api_docs/kbn_securitysolution_io_ts_types.mdx index 209109867e772..48fd0ca8ed815 100644 --- a/api_docs/kbn_securitysolution_io_ts_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_types.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-types title: "@kbn/securitysolution-io-ts-types" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-io-ts-types plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-types'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_io_ts_utils.mdx b/api_docs/kbn_securitysolution_io_ts_utils.mdx index cf783c9e2a9d7..571a2f9303ccb 100644 --- a/api_docs/kbn_securitysolution_io_ts_utils.mdx +++ b/api_docs/kbn_securitysolution_io_ts_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-utils title: "@kbn/securitysolution-io-ts-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-io-ts-utils plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_list_api.mdx b/api_docs/kbn_securitysolution_list_api.mdx index 4d54305c25a01..a0bcd043d61d4 100644 --- a/api_docs/kbn_securitysolution_list_api.mdx +++ b/api_docs/kbn_securitysolution_list_api.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-api title: "@kbn/securitysolution-list-api" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-list-api plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-api'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_list_constants.mdx b/api_docs/kbn_securitysolution_list_constants.mdx index d60c93054faec..993f080fcc193 100644 --- a/api_docs/kbn_securitysolution_list_constants.mdx +++ b/api_docs/kbn_securitysolution_list_constants.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-constants title: "@kbn/securitysolution-list-constants" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-list-constants plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-constants'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_list_hooks.mdx b/api_docs/kbn_securitysolution_list_hooks.mdx index e7a4be15ba900..85234c585d286 100644 --- a/api_docs/kbn_securitysolution_list_hooks.mdx +++ b/api_docs/kbn_securitysolution_list_hooks.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-hooks title: "@kbn/securitysolution-list-hooks" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-list-hooks plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-hooks'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_list_utils.mdx b/api_docs/kbn_securitysolution_list_utils.mdx index 548d3a6826a9f..c8d5be6aad2c5 100644 --- a/api_docs/kbn_securitysolution_list_utils.mdx +++ b/api_docs/kbn_securitysolution_list_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-utils title: "@kbn/securitysolution-list-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-list-utils plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_rules.mdx b/api_docs/kbn_securitysolution_rules.mdx index 24e05f4952126..96b51f9113c2e 100644 --- a/api_docs/kbn_securitysolution_rules.mdx +++ b/api_docs/kbn_securitysolution_rules.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-rules title: "@kbn/securitysolution-rules" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-rules plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-rules'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_t_grid.mdx b/api_docs/kbn_securitysolution_t_grid.mdx index ff18a7f3135c7..789760a4208e4 100644 --- a/api_docs/kbn_securitysolution_t_grid.mdx +++ b/api_docs/kbn_securitysolution_t_grid.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-t-grid title: "@kbn/securitysolution-t-grid" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-t-grid plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-t-grid'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_utils.mdx b/api_docs/kbn_securitysolution_utils.mdx index 186f5106c9ecc..f07604490c02d 100644 --- a/api_docs/kbn_securitysolution_utils.mdx +++ b/api_docs/kbn_securitysolution_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-utils title: "@kbn/securitysolution-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-utils plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_server_http_tools.mdx b/api_docs/kbn_server_http_tools.mdx index 1bd0fc10274f0..3b15b68770eda 100644 --- a/api_docs/kbn_server_http_tools.mdx +++ b/api_docs/kbn_server_http_tools.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-server-http-tools title: "@kbn/server-http-tools" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/server-http-tools plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-http-tools'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_server_route_repository.mdx b/api_docs/kbn_server_route_repository.mdx index ea8349c280bf2..af36112b1cda2 100644 --- a/api_docs/kbn_server_route_repository.mdx +++ b/api_docs/kbn_server_route_repository.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-server-route-repository title: "@kbn/server-route-repository" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/server-route-repository plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-route-repository'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_shared_ux_button_toolbar.mdx b/api_docs/kbn_shared_ux_button_toolbar.mdx index b0bf3309241cd..f6f742628e522 100644 --- a/api_docs/kbn_shared_ux_button_toolbar.mdx +++ b/api_docs/kbn_shared_ux_button_toolbar.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-toolbar title: "@kbn/shared-ux-button-toolbar" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/shared-ux-button-toolbar plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-toolbar'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_shared_ux_card_no_data.mdx b/api_docs/kbn_shared_ux_card_no_data.mdx index 4927ef7d422a6..baa5703955597 100644 --- a/api_docs/kbn_shared_ux_card_no_data.mdx +++ b/api_docs/kbn_shared_ux_card_no_data.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data title: "@kbn/shared-ux-card-no-data" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/shared-ux-card-no-data plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_shared_ux_components.mdx b/api_docs/kbn_shared_ux_components.mdx index 90f9d0da202d6..c856e7de45b63 100644 --- a/api_docs/kbn_shared_ux_components.mdx +++ b/api_docs/kbn_shared_ux_components.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-components title: "@kbn/shared-ux-components" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/shared-ux-components plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-components'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx index 25263bf851522..4723ab64fc68e 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data title: "@kbn/shared-ux-page-analytics-no-data" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/shared-ux-page-analytics-no-data plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx index e88217dcefe35..214a878423a93 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data title: "@kbn/shared-ux-page-kibana-no-data" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/shared-ux-page-kibana-no-data plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx index b92aac33cf0aa..b7fb30739d2b8 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views title: "@kbn/shared-ux-prompt-no-data-views" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/shared-ux-prompt-no-data-views plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_shared_ux_services.mdx b/api_docs/kbn_shared_ux_services.mdx index f809536fb2f63..5f60e287b33d3 100644 --- a/api_docs/kbn_shared_ux_services.mdx +++ b/api_docs/kbn_shared_ux_services.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-services title: "@kbn/shared-ux-services" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/shared-ux-services plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-services'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_shared_ux_storybook.mdx b/api_docs/kbn_shared_ux_storybook.mdx index 60f18e4f85447..eb278f6594d12 100644 --- a/api_docs/kbn_shared_ux_storybook.mdx +++ b/api_docs/kbn_shared_ux_storybook.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook title: "@kbn/shared-ux-storybook" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/shared-ux-storybook plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_shared_ux_utility.mdx b/api_docs/kbn_shared_ux_utility.mdx index 5dec81ad3ac9d..3cee3986320d0 100644 --- a/api_docs/kbn_shared_ux_utility.mdx +++ b/api_docs/kbn_shared_ux_utility.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-utility title: "@kbn/shared-ux-utility" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/shared-ux-utility plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-utility'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_sort_package_json.mdx b/api_docs/kbn_sort_package_json.mdx index 22052b556d703..42b1a1ec4b1b1 100644 --- a/api_docs/kbn_sort_package_json.mdx +++ b/api_docs/kbn_sort_package_json.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-sort-package-json title: "@kbn/sort-package-json" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/sort-package-json plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/sort-package-json'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_std.mdx b/api_docs/kbn_std.mdx index acfcc9383bb1c..7807dfb8faf19 100644 --- a/api_docs/kbn_std.mdx +++ b/api_docs/kbn_std.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-std title: "@kbn/std" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/std plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/std'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_stdio_dev_helpers.mdx b/api_docs/kbn_stdio_dev_helpers.mdx index 191e64cdf1b15..dcdbeeba9fa5c 100644 --- a/api_docs/kbn_stdio_dev_helpers.mdx +++ b/api_docs/kbn_stdio_dev_helpers.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-stdio-dev-helpers title: "@kbn/stdio-dev-helpers" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/stdio-dev-helpers plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/stdio-dev-helpers'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_storybook.mdx b/api_docs/kbn_storybook.mdx index 9915f6c0ffb52..72da8a866e68b 100644 --- a/api_docs/kbn_storybook.mdx +++ b/api_docs/kbn_storybook.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-storybook title: "@kbn/storybook" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/storybook plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/storybook'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_telemetry_tools.mdx b/api_docs/kbn_telemetry_tools.mdx index 1d9a712006716..6af8cd22263d4 100644 --- a/api_docs/kbn_telemetry_tools.mdx +++ b/api_docs/kbn_telemetry_tools.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-telemetry-tools title: "@kbn/telemetry-tools" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/telemetry-tools plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/telemetry-tools'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_test.devdocs.json b/api_docs/kbn_test.devdocs.json index b049321dd2e41..5ef0d045fbc3f 100644 --- a/api_docs/kbn_test.devdocs.json +++ b/api_docs/kbn_test.devdocs.json @@ -958,6 +958,87 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/test", + "id": "def-server.KbnClientRequesterError", + "type": "Class", + "tags": [], + "label": "KbnClientRequesterError", + "description": [], + "signature": [ + { + "pluginId": "@kbn/test", + "scope": "server", + "docId": "kibKbnTestPluginApi", + "section": "def-server.KbnClientRequesterError", + "text": "KbnClientRequesterError" + }, + " extends Error" + ], + "path": "packages/kbn-test/src/kbn_client/kbn_client_requester_error.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/test", + "id": "def-server.KbnClientRequesterError.axiosError", + "type": "Object", + "tags": [], + "label": "axiosError", + "description": [], + "signature": [ + "AxiosError", + " | undefined" + ], + "path": "packages/kbn-test/src/kbn_client/kbn_client_requester_error.ts", + "deprecated": false + }, + { + "parentPluginId": "@kbn/test", + "id": "def-server.KbnClientRequesterError.Unnamed", + "type": "Function", + "tags": [], + "label": "Constructor", + "description": [], + "signature": [ + "any" + ], + "path": "packages/kbn-test/src/kbn_client/kbn_client_requester_error.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/test", + "id": "def-server.KbnClientRequesterError.Unnamed.$1", + "type": "string", + "tags": [], + "label": "message", + "description": [], + "signature": [ + "string" + ], + "path": "packages/kbn-test/src/kbn_client/kbn_client_requester_error.ts", + "deprecated": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/test", + "id": "def-server.KbnClientRequesterError.Unnamed.$2", + "type": "Unknown", + "tags": [], + "label": "error", + "description": [], + "signature": [ + "unknown" + ], + "path": "packages/kbn-test/src/kbn_client/kbn_client_requester_error.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/test", "id": "def-server.Lifecycle", diff --git a/api_docs/kbn_test.mdx b/api_docs/kbn_test.mdx index 82c4660718088..5b96789883cea 100644 --- a/api_docs/kbn_test.mdx +++ b/api_docs/kbn_test.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-test title: "@kbn/test" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/test plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- @@ -18,7 +18,7 @@ Contact Operations for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 241 | 5 | 202 | 9 | +| 246 | 5 | 207 | 9 | ## Server diff --git a/api_docs/kbn_test_jest_helpers.mdx b/api_docs/kbn_test_jest_helpers.mdx index cbf66e2e32d12..8b3b115917f5f 100644 --- a/api_docs/kbn_test_jest_helpers.mdx +++ b/api_docs/kbn_test_jest_helpers.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-test-jest-helpers title: "@kbn/test-jest-helpers" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/test-jest-helpers plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-jest-helpers'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_tooling_log.mdx b/api_docs/kbn_tooling_log.mdx index f58eea4717a3c..d5d72ee68eb46 100644 --- a/api_docs/kbn_tooling_log.mdx +++ b/api_docs/kbn_tooling_log.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-tooling-log title: "@kbn/tooling-log" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/tooling-log plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/tooling-log'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_type_summarizer.mdx b/api_docs/kbn_type_summarizer.mdx index 8ae22eea4c48f..d9a99bc918996 100644 --- a/api_docs/kbn_type_summarizer.mdx +++ b/api_docs/kbn_type_summarizer.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-type-summarizer title: "@kbn/type-summarizer" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/type-summarizer plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/type-summarizer'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_typed_react_router_config.mdx b/api_docs/kbn_typed_react_router_config.mdx index 5de17b41dda5d..0d47f81985c33 100644 --- a/api_docs/kbn_typed_react_router_config.mdx +++ b/api_docs/kbn_typed_react_router_config.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-typed-react-router-config title: "@kbn/typed-react-router-config" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/typed-react-router-config plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/typed-react-router-config'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_ui_theme.mdx b/api_docs/kbn_ui_theme.mdx index 2a9a98da12496..afad507cc268f 100644 --- a/api_docs/kbn_ui_theme.mdx +++ b/api_docs/kbn_ui_theme.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-ui-theme title: "@kbn/ui-theme" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/ui-theme plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-theme'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_utility_types.mdx b/api_docs/kbn_utility_types.mdx index c0bf713a000c4..bd4ac2c281582 100644 --- a/api_docs/kbn_utility_types.mdx +++ b/api_docs/kbn_utility_types.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types title: "@kbn/utility-types" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/utility-types plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_utility_types_jest.mdx b/api_docs/kbn_utility_types_jest.mdx index f34e19084886d..ef3e2bead5c0e 100644 --- a/api_docs/kbn_utility_types_jest.mdx +++ b/api_docs/kbn_utility_types_jest.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types-jest title: "@kbn/utility-types-jest" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/utility-types-jest plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types-jest'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_utils.mdx b/api_docs/kbn_utils.mdx index 68a8d863e92c8..09a812a5932a1 100644 --- a/api_docs/kbn_utils.mdx +++ b/api_docs/kbn_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-utils title: "@kbn/utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/utils plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kibana_overview.mdx b/api_docs/kibana_overview.mdx index f0b4a3c578ae9..6b55b014d810e 100644 --- a/api_docs/kibana_overview.mdx +++ b/api_docs/kibana_overview.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kibanaOverview title: "kibanaOverview" image: https://source.unsplash.com/400x175/?github summary: API docs for the kibanaOverview plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaOverview'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kibana_react.devdocs.json b/api_docs/kibana_react.devdocs.json index 114c6531060a2..30705843ab0f9 100644 --- a/api_docs/kibana_react.devdocs.json +++ b/api_docs/kibana_react.devdocs.json @@ -4387,13 +4387,7 @@ "text": "SavedObjectsStart" }, " | undefined; i18n?: ", - { - "pluginId": "core", - "scope": "public", - "docId": "kibCorePluginApi", - "section": "def-public.I18nStart", - "text": "I18nStart" - }, + "I18nStart", " | undefined; notifications?: ", { "pluginId": "core", diff --git a/api_docs/kibana_react.mdx b/api_docs/kibana_react.mdx index bcdd5f22cab89..9a57e86eb9534 100644 --- a/api_docs/kibana_react.mdx +++ b/api_docs/kibana_react.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kibanaReact title: "kibanaReact" image: https://source.unsplash.com/400x175/?github summary: API docs for the kibanaReact plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaReact'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kibana_utils.mdx b/api_docs/kibana_utils.mdx index aad503a434db1..e7d76168b35b4 100644 --- a/api_docs/kibana_utils.mdx +++ b/api_docs/kibana_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kibanaUtils title: "kibanaUtils" image: https://source.unsplash.com/400x175/?github summary: API docs for the kibanaUtils plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaUtils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kubernetes_security.mdx b/api_docs/kubernetes_security.mdx index 4525bbe2e90ea..3533959bd49bd 100644 --- a/api_docs/kubernetes_security.mdx +++ b/api_docs/kubernetes_security.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kubernetesSecurity title: "kubernetesSecurity" image: https://source.unsplash.com/400x175/?github summary: API docs for the kubernetesSecurity plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kubernetesSecurity'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/lens.mdx b/api_docs/lens.mdx index f3a701a57c6f7..94aad70595690 100644 --- a/api_docs/lens.mdx +++ b/api_docs/lens.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/lens title: "lens" image: https://source.unsplash.com/400x175/?github summary: API docs for the lens plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lens'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/license_api_guard.mdx b/api_docs/license_api_guard.mdx index 20aa6358763ad..67ab1d0b38048 100644 --- a/api_docs/license_api_guard.mdx +++ b/api_docs/license_api_guard.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/licenseApiGuard title: "licenseApiGuard" image: https://source.unsplash.com/400x175/?github summary: API docs for the licenseApiGuard plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseApiGuard'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/license_management.mdx b/api_docs/license_management.mdx index 672bf37cfefb7..b9ca3227c0186 100644 --- a/api_docs/license_management.mdx +++ b/api_docs/license_management.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/licenseManagement title: "licenseManagement" image: https://source.unsplash.com/400x175/?github summary: API docs for the licenseManagement plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseManagement'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/licensing.mdx b/api_docs/licensing.mdx index 2d26f51d0b1d1..7768b99acd2d8 100644 --- a/api_docs/licensing.mdx +++ b/api_docs/licensing.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/licensing title: "licensing" image: https://source.unsplash.com/400x175/?github summary: API docs for the licensing plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licensing'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/lists.mdx b/api_docs/lists.mdx index c9226f6423611..0ad5f14873cda 100644 --- a/api_docs/lists.mdx +++ b/api_docs/lists.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/lists title: "lists" image: https://source.unsplash.com/400x175/?github summary: API docs for the lists plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lists'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/management.mdx b/api_docs/management.mdx index 85636d6db9aeb..f32d58cf2a204 100644 --- a/api_docs/management.mdx +++ b/api_docs/management.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/management title: "management" image: https://source.unsplash.com/400x175/?github summary: API docs for the management plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'management'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/maps.mdx b/api_docs/maps.mdx index e1a9268a4b430..c0ba7a0faca21 100644 --- a/api_docs/maps.mdx +++ b/api_docs/maps.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/maps title: "maps" image: https://source.unsplash.com/400x175/?github summary: API docs for the maps plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'maps'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/maps_ems.mdx b/api_docs/maps_ems.mdx index 6e53777799afe..9e2359a5d964e 100644 --- a/api_docs/maps_ems.mdx +++ b/api_docs/maps_ems.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/mapsEms title: "mapsEms" image: https://source.unsplash.com/400x175/?github summary: API docs for the mapsEms plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'mapsEms'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/ml.mdx b/api_docs/ml.mdx index 90e4c001a78b0..c1df1686a8069 100644 --- a/api_docs/ml.mdx +++ b/api_docs/ml.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/ml title: "ml" image: https://source.unsplash.com/400x175/?github summary: API docs for the ml plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ml'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/monitoring.mdx b/api_docs/monitoring.mdx index fe91552cf5304..13e932aff2c8c 100644 --- a/api_docs/monitoring.mdx +++ b/api_docs/monitoring.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/monitoring title: "monitoring" image: https://source.unsplash.com/400x175/?github summary: API docs for the monitoring plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoring'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/monitoring_collection.mdx b/api_docs/monitoring_collection.mdx index efbb5010c6689..1cdd404bf76bf 100644 --- a/api_docs/monitoring_collection.mdx +++ b/api_docs/monitoring_collection.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/monitoringCollection title: "monitoringCollection" image: https://source.unsplash.com/400x175/?github summary: API docs for the monitoringCollection plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoringCollection'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/navigation.mdx b/api_docs/navigation.mdx index 8a635d95c6698..165f0f335d986 100644 --- a/api_docs/navigation.mdx +++ b/api_docs/navigation.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/navigation title: "navigation" image: https://source.unsplash.com/400x175/?github summary: API docs for the navigation plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'navigation'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/newsfeed.mdx b/api_docs/newsfeed.mdx index 39103b8cd170f..a39288cf3cd8d 100644 --- a/api_docs/newsfeed.mdx +++ b/api_docs/newsfeed.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/newsfeed title: "newsfeed" image: https://source.unsplash.com/400x175/?github summary: API docs for the newsfeed plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'newsfeed'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/observability.mdx b/api_docs/observability.mdx index 8a3b10a8cce01..e2dcae516b104 100644 --- a/api_docs/observability.mdx +++ b/api_docs/observability.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/observability title: "observability" image: https://source.unsplash.com/400x175/?github summary: API docs for the observability plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observability'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/osquery.mdx b/api_docs/osquery.mdx index 55bd58456c64a..f86a267e95cec 100644 --- a/api_docs/osquery.mdx +++ b/api_docs/osquery.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/osquery title: "osquery" image: https://source.unsplash.com/400x175/?github summary: API docs for the osquery plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'osquery'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/plugin_directory.mdx b/api_docs/plugin_directory.mdx index 909b06aeb0f90..c71faef8e65e0 100644 --- a/api_docs/plugin_directory.mdx +++ b/api_docs/plugin_directory.mdx @@ -3,7 +3,7 @@ id: kibDevDocsPluginDirectory slug: /kibana-dev-docs/api-meta/plugin-api-directory title: Directory summary: Directory of public APIs available through plugins or packages. -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- @@ -12,13 +12,13 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Count | Plugins or Packages with a
public API | Number of teams | |--------------|----------|------------------------| -| 292 | 235 | 35 | +| 300 | 243 | 35 | ### Public API health stats | API Count | Any Count | Missing comments | Missing exports | |--------------|----------|-----------------|--------| -| 26363 | 171 | 19041 | 1244 | +| 26416 | 171 | 19085 | 1248 | ## Plugin Directory @@ -38,7 +38,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [Cloud Security Posture](https://github.com/orgs/elastic/teams/cloud-posture-security) | The cloud security posture plugin | 14 | 0 | 14 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 13 | 0 | 13 | 1 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The Controls Plugin contains embeddable components intended to create a simple query interface for end users, and a powerful editing suite that allows dashboard authors to build controls | 206 | 0 | 198 | 7 | -| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2527 | 15 | 940 | 29 | +| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2527 | 15 | 938 | 29 | | crossClusterReplication | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | | [Fleet](https://github.com/orgs/elastic/teams/fleet) | Add custom data integrations so they can be displayed in the Fleet integrations app | 101 | 0 | 82 | 1 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds the Dashboard app to Kibana | 143 | 0 | 141 | 12 | @@ -186,7 +186,8 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [Owner missing] | - | 18 | 0 | 9 | 1 | | | [Owner missing] | - | 4 | 0 | 4 | 0 | | | [Owner missing] | - | 7 | 0 | 2 | 0 | -| | [Owner missing] | - | 60 | 0 | 15 | 0 | +| | [Owner missing] | - | 3 | 0 | 3 | 0 | +| | [Owner missing] | - | 62 | 0 | 17 | 1 | | | [Owner missing] | - | 2 | 0 | 2 | 0 | | | [Owner missing] | - | 106 | 0 | 80 | 1 | | | [Owner missing] | - | 73 | 0 | 44 | 1 | @@ -195,13 +196,20 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [Owner missing] | - | 2 | 0 | 0 | 0 | | | [Owner missing] | - | 7 | 0 | 7 | 1 | | | [Owner missing] | - | 4 | 0 | 4 | 0 | +| | [Owner missing] | - | 3 | 0 | 0 | 0 | +| | [Owner missing] | - | 7 | 0 | 7 | 0 | +| | [Owner missing] | - | 5 | 0 | 5 | 0 | | | [Owner missing] | - | 3 | 0 | 3 | 0 | | | [Owner missing] | - | 12 | 0 | 3 | 0 | +| | [Owner missing] | - | 6 | 0 | 6 | 0 | | | [Owner missing] | - | 3 | 0 | 3 | 0 | +| | [Owner missing] | - | 15 | 0 | 13 | 0 | | | [Owner missing] | - | 4 | 0 | 4 | 0 | | | [Owner missing] | - | 4 | 0 | 4 | 0 | | | [Owner missing] | - | 5 | 0 | 2 | 0 | | | [Owner missing] | - | 4 | 0 | 4 | 0 | +| | [Owner missing] | - | 4 | 0 | 2 | 0 | +| | [Owner missing] | - | 3 | 0 | 3 | 0 | | | [Owner missing] | - | 8 | 2 | 6 | 0 | | | [Owner missing] | - | 4 | 0 | 4 | 0 | | | [Owner missing] | - | 56 | 0 | 30 | 0 | @@ -274,7 +282,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [Owner missing] | - | 4 | 0 | 2 | 0 | | | Operations | - | 38 | 2 | 21 | 0 | | | [Owner missing] | - | 2 | 0 | 2 | 0 | -| | Operations | - | 241 | 5 | 202 | 9 | +| | Operations | - | 246 | 5 | 207 | 9 | | | [Owner missing] | - | 135 | 8 | 103 | 2 | | | [Owner missing] | - | 72 | 0 | 55 | 0 | | | [Owner missing] | - | 29 | 0 | 2 | 0 | diff --git a/api_docs/presentation_util.mdx b/api_docs/presentation_util.mdx index bf2fe7cfebf46..19052227c3c90 100644 --- a/api_docs/presentation_util.mdx +++ b/api_docs/presentation_util.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/presentationUtil title: "presentationUtil" image: https://source.unsplash.com/400x175/?github summary: API docs for the presentationUtil plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'presentationUtil'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/remote_clusters.mdx b/api_docs/remote_clusters.mdx index 170032a3bc4d2..3729b362ce583 100644 --- a/api_docs/remote_clusters.mdx +++ b/api_docs/remote_clusters.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/remoteClusters title: "remoteClusters" image: https://source.unsplash.com/400x175/?github summary: API docs for the remoteClusters plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'remoteClusters'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/reporting.mdx b/api_docs/reporting.mdx index 5d3c8b0936a80..13556c6e80806 100644 --- a/api_docs/reporting.mdx +++ b/api_docs/reporting.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/reporting title: "reporting" image: https://source.unsplash.com/400x175/?github summary: API docs for the reporting plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'reporting'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/rollup.mdx b/api_docs/rollup.mdx index ca76c4653ec8e..f4fda7540b8a0 100644 --- a/api_docs/rollup.mdx +++ b/api_docs/rollup.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/rollup title: "rollup" image: https://source.unsplash.com/400x175/?github summary: API docs for the rollup plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'rollup'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/rule_registry.mdx b/api_docs/rule_registry.mdx index 5472f6eaed6b8..272a8758c5ab1 100644 --- a/api_docs/rule_registry.mdx +++ b/api_docs/rule_registry.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/ruleRegistry title: "ruleRegistry" image: https://source.unsplash.com/400x175/?github summary: API docs for the ruleRegistry plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ruleRegistry'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/runtime_fields.mdx b/api_docs/runtime_fields.mdx index 8f89583822b50..fabadde1d3537 100644 --- a/api_docs/runtime_fields.mdx +++ b/api_docs/runtime_fields.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/runtimeFields title: "runtimeFields" image: https://source.unsplash.com/400x175/?github summary: API docs for the runtimeFields plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'runtimeFields'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/saved_objects.mdx b/api_docs/saved_objects.mdx index f51a2a70b2220..d9fac2851b6d2 100644 --- a/api_docs/saved_objects.mdx +++ b/api_docs/saved_objects.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/savedObjects title: "savedObjects" image: https://source.unsplash.com/400x175/?github summary: API docs for the savedObjects plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjects'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/saved_objects_management.mdx b/api_docs/saved_objects_management.mdx index 0fbe05a13338f..79f1d2c7a2b4a 100644 --- a/api_docs/saved_objects_management.mdx +++ b/api_docs/saved_objects_management.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/savedObjectsManagement title: "savedObjectsManagement" image: https://source.unsplash.com/400x175/?github summary: API docs for the savedObjectsManagement plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsManagement'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/saved_objects_tagging.mdx b/api_docs/saved_objects_tagging.mdx index 1b39a76ecf0b7..38e12b4aed2cf 100644 --- a/api_docs/saved_objects_tagging.mdx +++ b/api_docs/saved_objects_tagging.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/savedObjectsTagging title: "savedObjectsTagging" image: https://source.unsplash.com/400x175/?github summary: API docs for the savedObjectsTagging plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTagging'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/saved_objects_tagging_oss.mdx b/api_docs/saved_objects_tagging_oss.mdx index 94134caede2d8..400d7bca2e21b 100644 --- a/api_docs/saved_objects_tagging_oss.mdx +++ b/api_docs/saved_objects_tagging_oss.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/savedObjectsTaggingOss title: "savedObjectsTaggingOss" image: https://source.unsplash.com/400x175/?github summary: API docs for the savedObjectsTaggingOss plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTaggingOss'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/screenshot_mode.mdx b/api_docs/screenshot_mode.mdx index cff973e270d3e..97e6cd138ec62 100644 --- a/api_docs/screenshot_mode.mdx +++ b/api_docs/screenshot_mode.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/screenshotMode title: "screenshotMode" image: https://source.unsplash.com/400x175/?github summary: API docs for the screenshotMode plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotMode'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/screenshotting.mdx b/api_docs/screenshotting.mdx index 263fbb99ec61f..2f4e0440757c1 100644 --- a/api_docs/screenshotting.mdx +++ b/api_docs/screenshotting.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/screenshotting title: "screenshotting" image: https://source.unsplash.com/400x175/?github summary: API docs for the screenshotting plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotting'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/security.mdx b/api_docs/security.mdx index 0b87aeb636f9f..99283269165de 100644 --- a/api_docs/security.mdx +++ b/api_docs/security.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/security title: "security" image: https://source.unsplash.com/400x175/?github summary: API docs for the security plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'security'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/security_solution.mdx b/api_docs/security_solution.mdx index 5b1e670ad236d..f9c4373d9fb2d 100644 --- a/api_docs/security_solution.mdx +++ b/api_docs/security_solution.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/securitySolution title: "securitySolution" image: https://source.unsplash.com/400x175/?github summary: API docs for the securitySolution plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'securitySolution'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/session_view.mdx b/api_docs/session_view.mdx index bfee9c84b1de8..47a076253b042 100644 --- a/api_docs/session_view.mdx +++ b/api_docs/session_view.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/sessionView title: "sessionView" image: https://source.unsplash.com/400x175/?github summary: API docs for the sessionView plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'sessionView'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/share.mdx b/api_docs/share.mdx index 2192eafe5e3f0..cd8bb647f2b8d 100644 --- a/api_docs/share.mdx +++ b/api_docs/share.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/share title: "share" image: https://source.unsplash.com/400x175/?github summary: API docs for the share plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'share'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/shared_u_x.mdx b/api_docs/shared_u_x.mdx index f64d2a3f4988b..d73c5303e9b74 100644 --- a/api_docs/shared_u_x.mdx +++ b/api_docs/shared_u_x.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/sharedUX title: "sharedUX" image: https://source.unsplash.com/400x175/?github summary: API docs for the sharedUX plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'sharedUX'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/snapshot_restore.mdx b/api_docs/snapshot_restore.mdx index 667c3941ee568..417c2500ed8cc 100644 --- a/api_docs/snapshot_restore.mdx +++ b/api_docs/snapshot_restore.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/snapshotRestore title: "snapshotRestore" image: https://source.unsplash.com/400x175/?github summary: API docs for the snapshotRestore plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'snapshotRestore'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/spaces.mdx b/api_docs/spaces.mdx index 793c6097c28af..af5231f6d3cd3 100644 --- a/api_docs/spaces.mdx +++ b/api_docs/spaces.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/spaces title: "spaces" image: https://source.unsplash.com/400x175/?github summary: API docs for the spaces plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'spaces'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/stack_alerts.mdx b/api_docs/stack_alerts.mdx index c751bd1600098..4395c11d84e4c 100644 --- a/api_docs/stack_alerts.mdx +++ b/api_docs/stack_alerts.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/stackAlerts title: "stackAlerts" image: https://source.unsplash.com/400x175/?github summary: API docs for the stackAlerts plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackAlerts'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/task_manager.mdx b/api_docs/task_manager.mdx index fbda9a1e539df..b734c5bfc99a0 100644 --- a/api_docs/task_manager.mdx +++ b/api_docs/task_manager.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/taskManager title: "taskManager" image: https://source.unsplash.com/400x175/?github summary: API docs for the taskManager plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'taskManager'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/telemetry.mdx b/api_docs/telemetry.mdx index ccf398abc8dbe..b2a40aa42d368 100644 --- a/api_docs/telemetry.mdx +++ b/api_docs/telemetry.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/telemetry title: "telemetry" image: https://source.unsplash.com/400x175/?github summary: API docs for the telemetry plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetry'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/telemetry_collection_manager.mdx b/api_docs/telemetry_collection_manager.mdx index 2f8e2e77085cf..a277dd510cb85 100644 --- a/api_docs/telemetry_collection_manager.mdx +++ b/api_docs/telemetry_collection_manager.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionManager title: "telemetryCollectionManager" image: https://source.unsplash.com/400x175/?github summary: API docs for the telemetryCollectionManager plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionManager'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/telemetry_collection_xpack.mdx b/api_docs/telemetry_collection_xpack.mdx index 9fd053ad4b8db..fa058e7406b50 100644 --- a/api_docs/telemetry_collection_xpack.mdx +++ b/api_docs/telemetry_collection_xpack.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionXpack title: "telemetryCollectionXpack" image: https://source.unsplash.com/400x175/?github summary: API docs for the telemetryCollectionXpack plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionXpack'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/telemetry_management_section.mdx b/api_docs/telemetry_management_section.mdx index 0704b8ce4782a..3e673801c2faf 100644 --- a/api_docs/telemetry_management_section.mdx +++ b/api_docs/telemetry_management_section.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/telemetryManagementSection title: "telemetryManagementSection" image: https://source.unsplash.com/400x175/?github summary: API docs for the telemetryManagementSection plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryManagementSection'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/timelines.mdx b/api_docs/timelines.mdx index 3056d7d683c84..1f32392bafbb5 100644 --- a/api_docs/timelines.mdx +++ b/api_docs/timelines.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/timelines title: "timelines" image: https://source.unsplash.com/400x175/?github summary: API docs for the timelines plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'timelines'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/transform.mdx b/api_docs/transform.mdx index 8de9d5e89de2d..2789c007d9ebd 100644 --- a/api_docs/transform.mdx +++ b/api_docs/transform.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/transform title: "transform" image: https://source.unsplash.com/400x175/?github summary: API docs for the transform plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'transform'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/triggers_actions_ui.mdx b/api_docs/triggers_actions_ui.mdx index a987e4b4bc8f3..af6efe1d4420a 100644 --- a/api_docs/triggers_actions_ui.mdx +++ b/api_docs/triggers_actions_ui.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/triggersActionsUi title: "triggersActionsUi" image: https://source.unsplash.com/400x175/?github summary: API docs for the triggersActionsUi plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'triggersActionsUi'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/ui_actions.mdx b/api_docs/ui_actions.mdx index c39a2718bf367..f242a77c877ca 100644 --- a/api_docs/ui_actions.mdx +++ b/api_docs/ui_actions.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/uiActions title: "uiActions" image: https://source.unsplash.com/400x175/?github summary: API docs for the uiActions plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActions'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/ui_actions_enhanced.mdx b/api_docs/ui_actions_enhanced.mdx index 1ccc372c28f52..d59b3f8d53f0e 100644 --- a/api_docs/ui_actions_enhanced.mdx +++ b/api_docs/ui_actions_enhanced.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/uiActionsEnhanced title: "uiActionsEnhanced" image: https://source.unsplash.com/400x175/?github summary: API docs for the uiActionsEnhanced plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActionsEnhanced'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/unified_search.mdx b/api_docs/unified_search.mdx index 426b8cf9fbf18..d6df7ac389983 100644 --- a/api_docs/unified_search.mdx +++ b/api_docs/unified_search.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/unifiedSearch title: "unifiedSearch" image: https://source.unsplash.com/400x175/?github summary: API docs for the unifiedSearch plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/unified_search_autocomplete.mdx b/api_docs/unified_search_autocomplete.mdx index cc30eb1fd7850..c2ea826c491b8 100644 --- a/api_docs/unified_search_autocomplete.mdx +++ b/api_docs/unified_search_autocomplete.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/unifiedSearch-autocomplete title: "unifiedSearch.autocomplete" image: https://source.unsplash.com/400x175/?github summary: API docs for the unifiedSearch.autocomplete plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch.autocomplete'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/url_forwarding.mdx b/api_docs/url_forwarding.mdx index a7ec086e66b1e..016ef659b8ba9 100644 --- a/api_docs/url_forwarding.mdx +++ b/api_docs/url_forwarding.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/urlForwarding title: "urlForwarding" image: https://source.unsplash.com/400x175/?github summary: API docs for the urlForwarding plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'urlForwarding'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/usage_collection.mdx b/api_docs/usage_collection.mdx index 47778833e072e..de91ebeef3794 100644 --- a/api_docs/usage_collection.mdx +++ b/api_docs/usage_collection.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/usageCollection title: "usageCollection" image: https://source.unsplash.com/400x175/?github summary: API docs for the usageCollection plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'usageCollection'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/ux.mdx b/api_docs/ux.mdx index 0058b11c6b0c8..6b85fe44c412e 100644 --- a/api_docs/ux.mdx +++ b/api_docs/ux.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/ux title: "ux" image: https://source.unsplash.com/400x175/?github summary: API docs for the ux plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ux'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/vis_default_editor.mdx b/api_docs/vis_default_editor.mdx index 53a3915ea7687..d7f1cffbb3585 100644 --- a/api_docs/vis_default_editor.mdx +++ b/api_docs/vis_default_editor.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visDefaultEditor title: "visDefaultEditor" image: https://source.unsplash.com/400x175/?github summary: API docs for the visDefaultEditor plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visDefaultEditor'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/vis_type_gauge.mdx b/api_docs/vis_type_gauge.mdx index a8243f4cdbc7e..e874cf81ef824 100644 --- a/api_docs/vis_type_gauge.mdx +++ b/api_docs/vis_type_gauge.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visTypeGauge title: "visTypeGauge" image: https://source.unsplash.com/400x175/?github summary: API docs for the visTypeGauge plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeGauge'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/vis_type_heatmap.mdx b/api_docs/vis_type_heatmap.mdx index 12ae5dddebc4a..724f26293f5e7 100644 --- a/api_docs/vis_type_heatmap.mdx +++ b/api_docs/vis_type_heatmap.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visTypeHeatmap title: "visTypeHeatmap" image: https://source.unsplash.com/400x175/?github summary: API docs for the visTypeHeatmap plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeHeatmap'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/vis_type_pie.mdx b/api_docs/vis_type_pie.mdx index 88bafeff23ed5..1535dda901b09 100644 --- a/api_docs/vis_type_pie.mdx +++ b/api_docs/vis_type_pie.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visTypePie title: "visTypePie" image: https://source.unsplash.com/400x175/?github summary: API docs for the visTypePie plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypePie'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/vis_type_table.mdx b/api_docs/vis_type_table.mdx index d97556bb2e801..eb1432e796dbd 100644 --- a/api_docs/vis_type_table.mdx +++ b/api_docs/vis_type_table.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visTypeTable title: "visTypeTable" image: https://source.unsplash.com/400x175/?github summary: API docs for the visTypeTable plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTable'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/vis_type_timelion.mdx b/api_docs/vis_type_timelion.mdx index 2055a6d670392..abebd148d2620 100644 --- a/api_docs/vis_type_timelion.mdx +++ b/api_docs/vis_type_timelion.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visTypeTimelion title: "visTypeTimelion" image: https://source.unsplash.com/400x175/?github summary: API docs for the visTypeTimelion plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimelion'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/vis_type_timeseries.mdx b/api_docs/vis_type_timeseries.mdx index d78d1cbbfbcb5..ef935085bd0e4 100644 --- a/api_docs/vis_type_timeseries.mdx +++ b/api_docs/vis_type_timeseries.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visTypeTimeseries title: "visTypeTimeseries" image: https://source.unsplash.com/400x175/?github summary: API docs for the visTypeTimeseries plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimeseries'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/vis_type_vega.mdx b/api_docs/vis_type_vega.mdx index 0275317068293..fde1cd3bcebd4 100644 --- a/api_docs/vis_type_vega.mdx +++ b/api_docs/vis_type_vega.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visTypeVega title: "visTypeVega" image: https://source.unsplash.com/400x175/?github summary: API docs for the visTypeVega plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVega'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/vis_type_vislib.mdx b/api_docs/vis_type_vislib.mdx index b3be9dae57a24..e06824be95340 100644 --- a/api_docs/vis_type_vislib.mdx +++ b/api_docs/vis_type_vislib.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visTypeVislib title: "visTypeVislib" image: https://source.unsplash.com/400x175/?github summary: API docs for the visTypeVislib plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVislib'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/vis_type_xy.mdx b/api_docs/vis_type_xy.mdx index 391ed29ea11d7..b34bd6b4905dd 100644 --- a/api_docs/vis_type_xy.mdx +++ b/api_docs/vis_type_xy.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visTypeXy title: "visTypeXy" image: https://source.unsplash.com/400x175/?github summary: API docs for the visTypeXy plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeXy'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/visualizations.mdx b/api_docs/visualizations.mdx index aa0a1affa5c38..a41c9bd8bbb59 100644 --- a/api_docs/visualizations.mdx +++ b/api_docs/visualizations.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visualizations title: "visualizations" image: https://source.unsplash.com/400x175/?github summary: API docs for the visualizations plugin -date: 2022-06-22 +date: 2022-06-23 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visualizations'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- From 8ae497afccabe6310cefb54d2f8a542be1d3ce43 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Thu, 23 Jun 2022 07:46:04 +0200 Subject: [PATCH 09/54] [Kibana Overview] First round of functional tests (#134680) * [Kibana Overview] First round of functional tests * Update tests * Update snapshot * Update failing test * Fix failing test * Fix failing test * Fix failing test * Fix failing test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .buildkite/ftr_configs.yml | 1 + .../__snapshots__/overview.test.tsx.snap | 40 +++++------ .../components/overview/overview.test.tsx | 10 +-- .../public/components/overview/overview.tsx | 10 +-- .../apps/kibana_overview/_analytics.ts | 63 ++++++++++++++++ .../apps/kibana_overview/_footer.ts | 55 ++++++++++++++ .../apps/kibana_overview/_no_data.ts | 42 +++++++++++ .../apps/kibana_overview/_page_header.ts | 69 ++++++++++++++++++ .../apps/kibana_overview/_solutions.ts | 72 +++++++++++++++++++ .../functional/apps/kibana_overview/config.ts | 18 +++++ test/functional/apps/kibana_overview/index.js | 23 ++++++ test/functional/config.base.js | 3 + 12 files changed, 373 insertions(+), 33 deletions(-) create mode 100644 test/functional/apps/kibana_overview/_analytics.ts create mode 100644 test/functional/apps/kibana_overview/_footer.ts create mode 100644 test/functional/apps/kibana_overview/_no_data.ts create mode 100644 test/functional/apps/kibana_overview/_page_header.ts create mode 100644 test/functional/apps/kibana_overview/_solutions.ts create mode 100644 test/functional/apps/kibana_overview/config.ts create mode 100644 test/functional/apps/kibana_overview/index.js diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index ba928931f303a..a824aa7ea29e2 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -73,6 +73,7 @@ enabled: - test/functional/apps/discover/config.ts - test/functional/apps/getting_started/config.ts - test/functional/apps/home/config.ts + - test/functional/apps/kibana_overview/config.ts - test/functional/apps/management/config.ts - test/functional/apps/saved_objects_management/config.ts - test/functional/apps/status_page/config.ts diff --git a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap index c185e1b85ae1a..a5b79401d0b46 100644 --- a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap +++ b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap @@ -1,22 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Overview during loading 1`] = ` -
-
- -
-
-`; - -exports[`Overview render 1`] = ` +exports[`Overview renders correctly 1`] = ` `; -exports[`Overview when there is no user data view 1`] = ` +exports[`Overview renders correctly when there is no user data view 1`] = ` `; -exports[`Overview without features 1`] = ` +exports[`Overview renders correctly without features 1`] = ` `; -exports[`Overview without solutions 1`] = ` +exports[`Overview renders correctly without solutions 1`] = ` `; + +exports[`Overview show loading spinner during loading 1`] = ` +
+
+ +
+
+`; diff --git a/src/plugins/kibana_overview/public/components/overview/overview.test.tsx b/src/plugins/kibana_overview/public/components/overview/overview.test.tsx index b433e7a39da13..99d8b45cdf27b 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.test.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.test.tsx @@ -166,7 +166,7 @@ describe('Overview', () => { afterAll(() => jest.clearAllMocks()); - test('render', async () => { + test('renders correctly', async () => { const component = mountWithIntl( { expect(component.find(KibanaPageTemplate).length).toBe(1); }); - test('without solutions', async () => { + test('renders correctly without solutions', async () => { const component = mountWithIntl( ); @@ -191,7 +191,7 @@ describe('Overview', () => { expect(component).toMatchSnapshot(); }); - test('without features', async () => { + test('renders correctly without features', async () => { const component = mountWithIntl( ); @@ -201,7 +201,7 @@ describe('Overview', () => { expect(component).toMatchSnapshot(); }); - test('when there is no user data view', async () => { + test('renders correctly when there is no user data view', async () => { hasESData.mockResolvedValue(true); hasUserDataView.mockResolvedValue(false); @@ -221,7 +221,7 @@ describe('Overview', () => { expect(component.find(EuiLoadingSpinner).length).toBe(0); }); - test('during loading', async () => { + test('show loading spinner during loading', async () => { hasESData.mockImplementation(() => new Promise(() => {})); hasUserDataView.mockImplementation(() => new Promise(() => {})); diff --git a/src/plugins/kibana_overview/public/components/overview/overview.tsx b/src/plugins/kibana_overview/public/components/overview/overview.tsx index 2258c662fe94e..738b278b17b36 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.tsx @@ -22,11 +22,11 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { CoreStart } from '@kbn/core/public'; import { useKibana, - KibanaPageTemplateSolutionNavAvatar, overviewPageActions, OverviewPageFooter, } from '@kbn/kibana-react-plugin/public'; import { KibanaPageTemplate } from '@kbn/shared-ux-components'; +import { KibanaSolutionAvatar } from '@kbn/shared-ux-avatar-solution'; import { AnalyticsNoDataPageKibanaProvider, AnalyticsNoDataPage, @@ -298,13 +298,7 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => className={`kbnOverviewSolution ${id}`} description={description ? description : ''} href={addBasePath(path)} - icon={ - - } + icon={} image={addBasePath(getSolutionGraphicURL(snakeCase(id)))} title={title} titleElement="h3" diff --git a/test/functional/apps/kibana_overview/_analytics.ts b/test/functional/apps/kibana_overview/_analytics.ts new file mode 100644 index 0000000000000..296256d924b24 --- /dev/null +++ b/test/functional/apps/kibana_overview/_analytics.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { WebElementWrapper } from '../../services/lib/web_element_wrapper'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const find = getService('find'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'header']); + + describe('overview page - Analytics apps', function describeIndexTests() { + before(async () => { + await esArchiver.load('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/kibana_sample_data_flights_index_pattern' + ); + await PageObjects.common.navigateToUrl('kibana_overview', '', { useActualUrl: true }); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + after(async () => { + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.unload( + 'test/functional/fixtures/kbn_archiver/kibana_sample_data_flights_index_pattern' + ); + }); + + const apps = ['dashboard', 'discover', 'canvas', 'maps', 'ml']; + + it('should display Analytics apps cards', async () => { + const kbnOverviewAppsCards = await find.allByCssSelector('.kbnOverviewApps__item'); + expect(kbnOverviewAppsCards.length).to.be(apps.length); + + const verifyImageUrl = async (el: WebElementWrapper, imgName: string) => { + const image = await el.findByCssSelector('img'); + const imageUrl = await image.getAttribute('src'); + expect(imageUrl.includes(imgName)).to.be(true); + }; + + for (let i = 0; i < apps.length; i++) { + verifyImageUrl(kbnOverviewAppsCards[i], `kibana_${apps[i]}_light.svg`); + } + }); + + it('click on a card should lead to the appropriate app', async () => { + const kbnOverviewAppsCards = await find.allByCssSelector('.kbnOverviewApps__item'); + const dashboardCard = kbnOverviewAppsCards.at(0); + expect(dashboardCard).not.to.be(undefined); + if (dashboardCard) { + await dashboardCard.click(); + await PageObjects.common.waitUntilUrlIncludes('app/dashboards'); + } + }); + }); +} diff --git a/test/functional/apps/kibana_overview/_footer.ts b/test/functional/apps/kibana_overview/_footer.ts new file mode 100644 index 0000000000000..c44d399154f14 --- /dev/null +++ b/test/functional/apps/kibana_overview/_footer.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const find = getService('find'); + const esArchiver = getService('esArchiver'); + const retry = getService('retry'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'header']); + + const defaultSettings = { + default_route: 'app/home', + }; + + describe('overview page - footer', function describeIndexTests() { + before(async () => { + await esArchiver.load('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/kibana_sample_data_flights_index_pattern' + ); + await PageObjects.common.navigateToUrl('kibana_overview', '', { useActualUrl: true }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await kibanaServer.uiSettings.replace(defaultSettings); + }); + + after(async () => { + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.unload( + 'test/functional/fixtures/kbn_archiver/kibana_sample_data_flights_index_pattern' + ); + await kibanaServer.uiSettings.replace(defaultSettings); + }); + + it('clicking footer updates landing page', async () => { + await PageObjects.header.waitUntilLoadingHasFinished(); + let footerButton = await find.byCssSelector('.kbnOverviewPageFooter__button'); + await footerButton.click(); + + await retry.try(async () => { + footerButton = await find.byCssSelector('.kbnOverviewPageFooter__button'); + const text = await ( + await footerButton.findByCssSelector('.euiButtonEmpty__text') + ).getVisibleText(); + expect(text.toString().includes('Display a different page on log in')).to.be(true); + }); + }); + }); +} diff --git a/test/functional/apps/kibana_overview/_no_data.ts b/test/functional/apps/kibana_overview/_no_data.ts new file mode 100644 index 0000000000000..8dec616eb8afe --- /dev/null +++ b/test/functional/apps/kibana_overview/_no_data.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const find = getService('find'); + const testSubjects = getService('testSubjects'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'header']); + + describe('overview page - no data', function describeIndexTests() { + before(async () => { + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + kibanaServer.savedObjects.clean({ types: ['index-pattern'] }); + await kibanaServer.importExport.unload( + 'test/functional/fixtures/kbn_archiver/kibana_sample_data_flights_index_pattern' + ); + await PageObjects.common.navigateToUrl('kibana_overview', '', { useActualUrl: true }); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + it('should display no data page', async () => { + await PageObjects.header.waitUntilLoadingHasFinished(); + const exists = await find.byClassName('kbnNoDataPageContents'); + expect(exists).not.to.be(undefined); + }); + + it('click on add data opens integrations', async () => { + const addIntegrations = await testSubjects.find('kbnOverviewAddIntegrations'); + await addIntegrations.click(); + await PageObjects.common.waitUntilUrlIncludes('integrations/browse'); + }); + }); +} diff --git a/test/functional/apps/kibana_overview/_page_header.ts b/test/functional/apps/kibana_overview/_page_header.ts new file mode 100644 index 0000000000000..8c254948676fc --- /dev/null +++ b/test/functional/apps/kibana_overview/_page_header.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const find = getService('find'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'header', 'dashboard']); + + describe('overview page - page header', function describeIndexTests() { + before(async () => { + await esArchiver.load('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/kibana_sample_data_flights_index_pattern' + ); + await PageObjects.common.navigateToUrl('kibana_overview', '', { useActualUrl: true }); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + after(async () => { + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.unload( + 'test/functional/fixtures/kbn_archiver/kibana_sample_data_flights_index_pattern' + ); + }); + + it('click on integrations leads to integrations', async () => { + const headerItems = await find.byCssSelector('.euiPageHeaderContent__rightSideItems'); + const items = await headerItems.findAllByCssSelector('.kbnRedirectCrossAppLinks'); + expect(items!.length).to.be(3); + + const integrations = await items!.at(0); + await integrations!.click(); + await PageObjects.common.waitUntilUrlIncludes('app/integrations/browse'); + }); + + it('click on management leads to management', async () => { + await PageObjects.common.navigateToUrl('kibana_overview', '', { useActualUrl: true }); + await PageObjects.header.waitUntilLoadingHasFinished(); + + const headerItems = await find.byCssSelector('.euiPageHeaderContent__rightSideItems'); + const items = await headerItems.findAllByCssSelector('.kbnRedirectCrossAppLinks'); + + const management = await items!.at(1); + await management!.click(); + await PageObjects.common.waitUntilUrlIncludes('app/management'); + }); + + it('click on dev tools leads to dev tools', async () => { + await PageObjects.common.navigateToUrl('kibana_overview', '', { useActualUrl: true }); + await PageObjects.header.waitUntilLoadingHasFinished(); + + const headerItems = await find.byCssSelector('.euiPageHeaderContent__rightSideItems'); + const items = await headerItems.findAllByCssSelector('.kbnRedirectCrossAppLinks'); + + const devTools = await items!.at(2); + await devTools!.click(); + await PageObjects.common.waitUntilUrlIncludes('app/dev_tools'); + }); + }); +} diff --git a/test/functional/apps/kibana_overview/_solutions.ts b/test/functional/apps/kibana_overview/_solutions.ts new file mode 100644 index 0000000000000..97af7f7242eff --- /dev/null +++ b/test/functional/apps/kibana_overview/_solutions.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const find = getService('find'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const retry = getService('retry'); + const PageObjects = getPageObjects(['common', 'header']); + + describe('overview page - solutions', function describeIndexTests() { + before(async () => { + await esArchiver.load('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/kibana_sample_data_flights_index_pattern' + ); + await PageObjects.common.navigateToUrl('kibana_overview', '', { useActualUrl: true }); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + after(async () => { + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.unload( + 'test/functional/fixtures/kbn_archiver/kibana_sample_data_flights_index_pattern' + ); + }); + + it('contains the appropriate solutions', async () => { + const solutionCards = await find.allByCssSelector('.kbnOverviewMore__item'); + expect(solutionCards.length).to.be(2); + + const observabilityImage = await solutionCards[0].findByCssSelector('img'); + const observabilityImageUrl = await observabilityImage.getAttribute('src'); + expect(observabilityImageUrl.includes('/solutions_observability.svg')).to.be(true); + + const securityImage = await solutionCards[1].findByCssSelector('img'); + const securityImageUrl = await securityImage.getAttribute('src'); + expect(securityImageUrl.includes('/solutions_security_solution.svg')).to.be(true); + }); + + it('click on Observability card leads to Observability', async () => { + let solutionCards: string | any[] = []; + await retry.waitForWithTimeout('all solutions to be present', 5000, async () => { + solutionCards = await find.allByCssSelector('.kbnOverviewMore__item'); + return solutionCards.length === 2; + }); + await solutionCards[0].click(); + await PageObjects.common.waitUntilUrlIncludes('app/observability'); + }); + + it('click on Security card leads to Security', async () => { + await PageObjects.common.navigateToUrl('kibana_overview', '', { useActualUrl: true }); + await PageObjects.header.waitUntilLoadingHasFinished(); + + let solutionCards: string | any[] = []; + await retry.waitForWithTimeout('all solutions to be present', 5000, async () => { + solutionCards = await find.allByCssSelector('.kbnOverviewMore__item'); + return solutionCards.length === 2; + }); + await solutionCards[1].click(); + await PageObjects.common.waitUntilUrlIncludes('app/security'); + }); + }); +} diff --git a/test/functional/apps/kibana_overview/config.ts b/test/functional/apps/kibana_overview/config.ts new file mode 100644 index 0000000000000..e487d31dcb657 --- /dev/null +++ b/test/functional/apps/kibana_overview/config.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/kibana_overview/index.js b/test/functional/apps/kibana_overview/index.js new file mode 100644 index 0000000000000..40dd47f6b69ef --- /dev/null +++ b/test/functional/apps/kibana_overview/index.js @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export default function ({ getService, loadTestFile }) { + const browser = getService('browser'); + + describe('kibana overview app', function () { + before(function () { + return browser.setWindowSize(1200, 800); + }); + + loadTestFile(require.resolve('./_no_data')); + loadTestFile(require.resolve('./_page_header')); + loadTestFile(require.resolve('./_analytics')); + loadTestFile(require.resolve('./_solutions')); + loadTestFile(require.resolve('./_footer')); + }); +} diff --git a/test/functional/config.base.js b/test/functional/config.base.js index 147fef2685f5d..f7f210aa7de32 100644 --- a/test/functional/config.base.js +++ b/test/functional/config.base.js @@ -90,6 +90,9 @@ export default async function ({ readConfigFile }) { integrations: { pathname: '/app/integrations', }, + kibana_overview: { + pathname: '/app/kibana_overview', + }, }, junit: { reportName: 'Chrome UI Functional Tests', From bf65d4c261b0e344c13c474a59347a3aa7b0a21d Mon Sep 17 00:00:00 2001 From: Ioana Tagirta Date: Thu, 23 Jun 2022 09:04:41 +0200 Subject: [PATCH 10/54] Enterprise Search: change route for curations/find_or_create (#134894) * Change route for curations/find_or_create * Fix tests --- .../components/analytics_tables/shared_columns.tsx | 4 ++-- .../test_helpers/shared_columns_tests.tsx | 14 +++++++------- .../server/routes/app_search/curations.test.ts | 8 ++++---- .../server/routes/app_search/curations.ts | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx index e7c98332ee651..b4ddf18b8adb0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx @@ -86,9 +86,9 @@ export const ACTIONS_COLUMN = { try { const query = (item as Query).key || (item as RecentQuery).query_string || '""'; - const response = await http.get<{ id: string }>( + const response = await http.post<{ id: string }>( `/internal/app_search/engines/${engineName}/curations/find_or_create`, - { query: { query } } + { body: JSON.stringify({ query }) } ); navigateToUrl(generateEnginePath(ENGINE_CURATION_PATH, { curationId: response.id })); } catch (e) { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/test_helpers/shared_columns_tests.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/test_helpers/shared_columns_tests.tsx index d9ffb83a561c4..3033c1dcbeea0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/test_helpers/shared_columns_tests.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/test_helpers/shared_columns_tests.tsx @@ -44,35 +44,35 @@ export const runActionColumnTests = (wrapper: ReactWrapper) => { describe('edit action', () => { it('calls the find_or_create curation API, then navigates the user to the curation', async () => { - http.get.mockReturnValue(Promise.resolve({ id: 'cur-123456789' })); + http.post.mockReturnValue(Promise.resolve({ id: 'cur-123456789' })); wrapper.find('[data-test-subj="AnalyticsTableEditQueryButton"]').first().simulate('click'); await nextTick(); - expect(http.get).toHaveBeenCalledWith( + expect(http.post).toHaveBeenCalledWith( '/internal/app_search/engines/some-engine/curations/find_or_create', { - query: { query: 'some search' }, + body: JSON.stringify({ query: 'some search' }), } ); expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations/cur-123456789'); }); it('falls back to "" for the empty query', async () => { - http.get.mockReturnValue(Promise.resolve({ id: 'cur-987654321' })); + http.post.mockReturnValue(Promise.resolve({ id: 'cur-987654321' })); wrapper.find('[data-test-subj="AnalyticsTableEditQueryButton"]').last().simulate('click'); await nextTick(); - expect(http.get).toHaveBeenCalledWith( + expect(http.post).toHaveBeenCalledWith( '/internal/app_search/engines/some-engine/curations/find_or_create', { - query: { query: '""' }, + body: JSON.stringify({ query: '""' }), } ); expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations/cur-987654321'); }); it('handles API errors', async () => { - http.get.mockReturnValue(Promise.reject()); + http.post.mockReturnValue(Promise.reject()); wrapper.find('[data-test-subj="AnalyticsTableEditQueryButton"]').first().simulate('click'); await nextTick(); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/curations.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/curations.test.ts index b930b449e97d1..8e2221b8d8f32 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/curations.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/curations.test.ts @@ -195,13 +195,13 @@ describe('curations routes', () => { }); }); - describe('GET /internal/app_search/engines/{engineName}/curations/find_or_create', () => { + describe('POST /internal/app_search/engines/{engineName}/curations/find_or_create', () => { let mockRouter: MockRouter; beforeEach(() => { jest.clearAllMocks(); mockRouter = new MockRouter({ - method: 'get', + method: 'post', path: '/internal/app_search/engines/{engineName}/curations/find_or_create', }); @@ -219,12 +219,12 @@ describe('curations routes', () => { describe('validates', () => { it('required query param', () => { - const request = { query: { query: 'some query' } }; + const request = { body: { query: 'some query' } }; mockRouter.shouldValidate(request); }); it('missing query', () => { - const request = { query: {} }; + const request = { body: {} }; mockRouter.shouldThrow(request); }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/curations.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/curations.ts index a7282e5dc6cc4..27927f2c36913 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/curations.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/curations.ts @@ -105,14 +105,14 @@ export function registerCurationsRoutes({ }) ); - router.get( + router.post( { path: '/internal/app_search/engines/{engineName}/curations/find_or_create', validate: { params: schema.object({ engineName: schema.string(), }), - query: schema.object({ + body: schema.object({ query: schema.string(), }), }, From d11c0be46577b10e23e87c91ff8997978fbf8d19 Mon Sep 17 00:00:00 2001 From: Marta Bondyra <4283304+mbondyra@users.noreply.github.com> Date: Thu, 23 Jun 2022 09:32:19 +0200 Subject: [PATCH 11/54] [Lens] Implement drag and drop between layers (#132018) * added dnd between layers * Andrew's comments addressed Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Stratoula Kalafateli --- .../lens/public/drag_drop/drag_drop.test.tsx | 61 +- .../drag_drop/providers/announcements.tsx | 309 ++- .../lens/public/drag_drop/providers/types.tsx | 1 + .../buttons/draggable_dimension_button.tsx | 53 +- .../buttons/drop_targets_utils.test.tsx | 123 + .../buttons/drop_targets_utils.tsx | 111 +- .../buttons/empty_dimension_button.tsx | 64 +- .../config_panel/layer_panel.test.tsx | 112 +- .../editor_frame/config_panel/layer_panel.tsx | 161 +- .../editor_frame/config_panel/types.ts | 1 - .../droppable/droppable.test.ts | 2332 ----------------- .../droppable/get_drop_props.test.ts | 767 ++++++ .../droppable/get_drop_props.ts | 214 +- .../dimension_panel/droppable/mocks.ts | 292 +++ .../droppable/on_drop_handler.test.ts | 2259 ++++++++++++++++ .../droppable/on_drop_handler.ts | 648 +++-- .../dimension_panel/operation_support.ts | 6 +- .../operations/definitions/count.tsx | 8 +- .../definitions/formula/formula.tsx | 24 +- .../operations/definitions/formula/math.tsx | 4 +- .../operations/definitions/index.ts | 22 +- .../operations/definitions/metrics.tsx | 4 +- .../operations/definitions/static_value.tsx | 21 +- .../operations/definitions/terms/index.tsx | 2 +- .../definitions/terms/terms.test.tsx | 21 +- .../operations/layer_helpers.test.ts | 49 +- .../operations/layer_helpers.ts | 163 +- .../operations/time_scale_utils.test.ts | 23 +- .../operations/time_scale_utils.ts | 3 +- .../indexpattern_datasource/state_helpers.ts | 16 + .../public/indexpattern_datasource/types.ts | 6 + x-pack/plugins/lens/public/types.ts | 51 +- .../xy_visualization/annotations/helpers.tsx | 252 +- .../reference_line_helpers.tsx | 6 +- .../xy_visualization/visualization.test.ts | 234 +- .../public/xy_visualization/visualization.tsx | 18 +- .../visualization_helpers.tsx | 1 + .../translations/translations/fr-FR.json | 34 - .../translations/translations/ja-JP.json | 29 - .../translations/translations/zh-CN.json | 34 - x-pack/test/accessibility/apps/lens.ts | 26 +- .../apps/lens/group1/smokescreen.ts | 52 +- .../test/functional/apps/lens/group1/table.ts | 8 +- .../functional/apps/lens/group2/dashboard.ts | 26 +- .../apps/lens/group3/annotations.ts | 8 +- .../apps/lens/group3/drag_and_drop.ts | 171 +- .../apps/lens/group3/error_handling.ts | 8 +- .../functional/apps/lens/group3/formula.ts | 31 +- .../apps/lens/group3/reference_lines.ts | 16 +- .../test/functional/page_objects/lens_page.ts | 30 +- 50 files changed, 5424 insertions(+), 3491 deletions(-) create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.test.tsx delete mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.test.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/mocks.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.test.ts diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx index 1d6c14c09136a..c0d7766fc22d8 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx @@ -41,7 +41,14 @@ describe('DragDrop', () => { const value = { id: '1', - humanData: { label: 'hello', groupLabel: 'X', position: 1, canSwap: true, canDuplicate: true }, + humanData: { + label: 'hello', + groupLabel: 'X', + position: 1, + canSwap: true, + canDuplicate: true, + layerNumber: 0, + }, }; test('renders if nothing is being dragged', () => { @@ -205,7 +212,7 @@ describe('DragDrop', () => { order={[2, 0, 1, 0]} onDrop={(x: unknown) => {}} dropTypes={undefined} - value={{ id: '2', humanData: { label: 'label2' } }} + value={{ id: '2', humanData: { label: 'label2', layerNumber: 0 } }} > @@ -231,7 +238,7 @@ describe('DragDrop', () => { }} > @@ -286,7 +293,7 @@ describe('DragDrop', () => { registerDropTarget={jest.fn()} > @@ -329,7 +336,7 @@ describe('DragDrop', () => { draggable: true, value: { id: '1', - humanData: { label: 'Label1', position: 1 }, + humanData: { label: 'Label1', position: 1, layerNumber: 0 }, }, children: '1', order: [2, 0, 0, 0], @@ -341,7 +348,7 @@ describe('DragDrop', () => { value: { id: '2', - humanData: { label: 'label2', position: 1 }, + humanData: { label: 'label2', position: 1, layerNumber: 0 }, }, onDrop, dropTypes: ['move_compatible'] as DropType[], @@ -358,6 +365,7 @@ describe('DragDrop', () => { groupLabel: 'Y', canSwap: true, canDuplicate: true, + layerNumber: 0, }, }, onDrop, @@ -373,7 +381,7 @@ describe('DragDrop', () => { dragType: 'move' as 'copy' | 'move', value: { id: '4', - humanData: { label: 'label4', position: 2, groupLabel: 'Y' }, + humanData: { label: 'label4', position: 2, groupLabel: 'Y', layerNumber: 0 }, }, order: [2, 0, 2, 1], }, @@ -415,11 +423,11 @@ describe('DragDrop', () => { }); keyboardHandler.simulate('keydown', { key: 'Enter' }); expect(setA11yMessage).toBeCalledWith( - `You're dragging Label1 from at position 1 over label3 from Y group at position 1. Press space or enter to replace label3 with Label1. Hold alt or option to duplicate. Hold shift to swap.` + `You're dragging Label1 from at position 1 in layer 0 over label3 from Y group at position 1 in layer 0. Press space or enter to replace label3 with Label1. Hold alt or option to duplicate. Hold shift to swap.` ); expect(setActiveDropTarget).toBeCalledWith(undefined); expect(onDrop).toBeCalledWith( - { humanData: { label: 'Label1', position: 1 }, id: '1' }, + { humanData: { label: 'Label1', position: 1, layerNumber: 0 }, id: '1' }, 'move_compatible' ); }); @@ -474,7 +482,7 @@ describe('DragDrop', () => { draggable: true, value: { id: '1', - humanData: { label: 'Label1', position: 1 }, + humanData: { label: 'Label1', position: 1, layerNumber: 0 }, }, children: '1', order: [2, 0, 0, 0], @@ -486,7 +494,7 @@ describe('DragDrop', () => { value: { id: '2', - humanData: { label: 'label2', position: 1 }, + humanData: { label: 'label2', position: 1, layerNumber: 0 }, }, onDrop, dropTypes: ['move_compatible'] as DropType[], @@ -533,7 +541,7 @@ describe('DragDrop', () => { component = mount( { registerDropTarget={jest.fn()} > @@ -629,18 +637,24 @@ describe('DragDrop', () => { component.find('SingleDropInner').at(0).simulate('dragover'); component.find('SingleDropInner').at(0).simulate('drop'); - expect(onDrop).toBeCalledWith({ humanData: { label: 'Label1' }, id: '1' }, 'move_compatible'); + expect(onDrop).toBeCalledWith( + { humanData: { label: 'Label1', layerNumber: 0 }, id: '1' }, + 'move_compatible' + ); component.find('SingleDropInner').at(1).simulate('dragover'); component.find('SingleDropInner').at(1).simulate('drop'); expect(onDrop).toBeCalledWith( - { humanData: { label: 'Label1' }, id: '1' }, + { humanData: { label: 'Label1', layerNumber: 0 }, id: '1' }, 'duplicate_compatible' ); component.find('SingleDropInner').at(2).simulate('dragover'); component.find('SingleDropInner').at(2).simulate('drop'); - expect(onDrop).toBeCalledWith({ humanData: { label: 'Label1' }, id: '1' }, 'swap_compatible'); + expect(onDrop).toBeCalledWith( + { humanData: { label: 'Label1', layerNumber: 0 }, id: '1' }, + 'swap_compatible' + ); }); test('pressing Alt or Shift when dragging over the main drop target sets extra drop target as active', () => { @@ -693,7 +707,7 @@ describe('DragDrop', () => { draggable: true, value: { id: '1', - humanData: { label: 'Label1', position: 1 }, + humanData: { label: 'Label1', position: 1, layerNumber: 0 }, }, children: '1', order: [2, 0, 0, 0], @@ -705,7 +719,7 @@ describe('DragDrop', () => { value: { id: '2', - humanData: { label: 'label2', position: 1 }, + humanData: { label: 'label2', position: 1, layerNumber: 0 }, }, onDrop, dropTypes: ['move_compatible', 'duplicate_compatible', 'swap_compatible'] as DropType[], @@ -716,7 +730,7 @@ describe('DragDrop', () => { dragType: 'move' as const, value: { id: '3', - humanData: { label: 'label3', position: 1, groupLabel: 'Y' }, + humanData: { label: 'label3', position: 1, groupLabel: 'Y', layerNumber: 0 }, }, onDrop, dropTypes: ['replace_compatible'] as DropType[], @@ -734,6 +748,7 @@ describe('DragDrop', () => { humanData: { label: 'label2', position: 1, + layerNumber: 0, }, id: '2', onDrop, @@ -743,6 +758,7 @@ describe('DragDrop', () => { humanData: { label: 'label2', position: 1, + layerNumber: 0, }, id: '2', onDrop, @@ -753,6 +769,7 @@ describe('DragDrop', () => { groupLabel: 'Y', label: 'label3', position: 1, + layerNumber: 0, }, id: '3', onDrop, @@ -942,18 +959,18 @@ describe('DragDrop', () => { const items = [ { id: '1', - humanData: { label: 'Label1', position: 1, groupLabel: 'X' }, + humanData: { label: 'Label1', position: 1, groupLabel: 'X', layerNumber: 0 }, onDrop, draggable: true, }, { id: '2', - humanData: { label: 'label2', position: 2, groupLabel: 'X' }, + humanData: { label: 'label2', position: 2, groupLabel: 'X', layerNumber: 0 }, onDrop, }, { id: '3', - humanData: { label: 'label3', position: 3, groupLabel: 'X' }, + humanData: { label: 'label3', position: 3, groupLabel: 'X', layerNumber: 0 }, onDrop, }, ]; diff --git a/x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx b/x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx index 0d247825b6a17..a21656421a96f 100644 --- a/x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx @@ -22,19 +22,21 @@ interface CustomAnnouncementsType { const replaceAnnouncement = { selectedTarget: ( - { label, groupLabel, position }: HumanData, + { label, groupLabel, position, layerNumber }: HumanData, { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition, canSwap, canDuplicate, + canCombine, + layerNumber: dropLayerNumber, }: HumanData, announceModifierKeys?: boolean ) => { if (announceModifierKeys && (canSwap || canDuplicate)) { return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replaceMain', { - defaultMessage: `You're dragging {label} from {groupLabel} at position {position} over {dropLabel} from {dropGroupLabel} group at position {dropPosition}. Press space or enter to replace {dropLabel} with {label}.{duplicateCopy}{swapCopy}`, + defaultMessage: `You're dragging {label} from {groupLabel} at position {position} in layer {layerNumber} over {dropLabel} from {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Press space or enter to replace {dropLabel} with {label}.{duplicateCopy}{swapCopy}{combineCopy}`, values: { label, groupLabel, @@ -44,63 +46,76 @@ const replaceAnnouncement = { dropPosition, duplicateCopy: canDuplicate ? DUPLICATE_SHORT : '', swapCopy: canSwap ? SWAP_SHORT : '', + combineCopy: canCombine ? COMBINE_SHORT : '', + layerNumber, + dropLayerNumber, }, }); } - return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replace', { - defaultMessage: `Replace {dropLabel} in {dropGroupLabel} group at position {dropPosition} with {label}. Press space or enter to replace.`, + defaultMessage: `Replace {dropLabel} in {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber} with {label}. Press space or enter to replace.`, values: { label, dropLabel, dropGroupLabel, dropPosition, + dropLayerNumber, }, }); }, - dropped: ({ label }: HumanData, { label: dropLabel, groupLabel, position }: HumanData) => - i18n.translate('xpack.lens.dragDrop.announce.duplicated.replace', { - defaultMessage: 'Replaced {dropLabel} with {label} in {groupLabel} at position {position}', + dropped: ( + { label }: HumanData, + { label: dropLabel, groupLabel, position, layerNumber: dropLayerNumber }: HumanData + ) => { + return i18n.translate('xpack.lens.dragDrop.announce.duplicated.replace', { + defaultMessage: + 'Replaced {dropLabel} with {label} in {groupLabel} at position {position} in layer {dropLayerNumber}', values: { label, dropLabel, groupLabel, position, + dropLayerNumber, }, - }), + }); + }, }; const duplicateAnnouncement = { selectedTarget: ( - { label, groupLabel }: HumanData, + { label, groupLabel, layerNumber }: HumanData, { groupLabel: dropGroupLabel, position }: HumanData ) => { if (groupLabel !== dropGroupLabel) { return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.duplicated', { - defaultMessage: `Duplicate {label} to {dropGroupLabel} group at position {position}. Hold Alt or Option and press space or enter to duplicate`, + defaultMessage: `Duplicate {label} to {dropGroupLabel} group at position {position} in layer {layerNumber}. Hold Alt or Option and press space or enter to duplicate`, values: { label, dropGroupLabel, position, + layerNumber, }, }); } return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.duplicatedInGroup', { - defaultMessage: `Duplicate {label} to {dropGroupLabel} group at position {position}. Press space or enter to duplicate`, + defaultMessage: `Duplicate {label} to {dropGroupLabel} group at position {position} in layer {layerNumber}. Press space or enter to duplicate`, values: { label, dropGroupLabel, position, + layerNumber, }, }); }, - dropped: ({ label }: HumanData, { groupLabel, position }: HumanData) => + dropped: ({ label }: HumanData, { groupLabel, position, layerNumber }: HumanData) => i18n.translate('xpack.lens.dragDrop.announce.dropped.duplicated', { - defaultMessage: 'Duplicated {label} in {groupLabel} group at position {position}', + defaultMessage: + 'Duplicated {label} in {groupLabel} group at position {position} in layer {layerNumber}', values: { label, groupLabel, position, + layerNumber, }, }), }; @@ -109,8 +124,8 @@ const reorderAnnouncement = { selectedTarget: ( { label, groupLabel, position: prevPosition }: HumanData, { position }: HumanData - ) => - prevPosition === position + ) => { + return prevPosition === position ? i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.reorderedBack', { defaultMessage: `{label} returned to its initial position {prevPosition}`, values: { @@ -121,12 +136,13 @@ const reorderAnnouncement = { : i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.reordered', { defaultMessage: `Reorder {label} in {groupLabel} group from position {prevPosition} to position {position}. Press space or enter to reorder`, values: { - groupLabel, label, + groupLabel, position, prevPosition, }, - }), + }); + }, dropped: ({ label, groupLabel, position: prevPosition }: HumanData, { position }: HumanData) => i18n.translate('xpack.lens.dragDrop.announce.dropped.reordered', { defaultMessage: @@ -142,7 +158,7 @@ const reorderAnnouncement = { const combineAnnouncement = { selectedTarget: ( - { label, groupLabel, position }: HumanData, + { label, groupLabel, position, layerNumber }: HumanData, { label: dropLabel, groupLabel: dropGroupLabel, @@ -150,12 +166,13 @@ const combineAnnouncement = { canSwap, canDuplicate, canCombine, + layerNumber: dropLayerNumber, }: HumanData, announceModifierKeys?: boolean ) => { if (announceModifierKeys && (canSwap || canDuplicate || canCombine)) { return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.combineMain', { - defaultMessage: `You're dragging {label} from {groupLabel} at position {position} over {dropLabel} from {dropGroupLabel} group at position {dropPosition}. Press space or enter to combine {dropLabel} with {label}.{duplicateCopy}{swapCopy}{combineCopy}`, + defaultMessage: `You're dragging {label} from {groupLabel} at position {position} in layer {layerNumber} over {dropLabel} from {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Press space or enter to combine {dropLabel} with {label}.{duplicateCopy}{swapCopy}{combineCopy}`, values: { label, groupLabel, @@ -166,28 +183,35 @@ const combineAnnouncement = { duplicateCopy: canDuplicate ? DUPLICATE_SHORT : '', swapCopy: canSwap ? SWAP_SHORT : '', combineCopy: canCombine ? COMBINE_SHORT : '', + layerNumber, + dropLayerNumber, }, }); } - return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.combine', { - defaultMessage: `Combine {dropLabel} in {dropGroupLabel} group at position {dropPosition} with {label}. Press space or enter to combine.`, + defaultMessage: `Combine {dropLabel} in {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber} with {label}. Press space or enter to combine.`, values: { label, dropLabel, dropGroupLabel, dropPosition, + dropLayerNumber, }, }); }, - dropped: ({ label }: HumanData, { label: dropLabel, groupLabel, position }: HumanData) => + dropped: ( + { label }: HumanData, + { label: dropLabel, groupLabel, position, layerNumber: dropLayerNumber }: HumanData + ) => i18n.translate('xpack.lens.dragDrop.announce.duplicated.combine', { - defaultMessage: 'Combine {dropLabel} with {label} in {groupLabel} at position {position}', + defaultMessage: + 'Combine {dropLabel} with {label} in {groupLabel} at position {position} in layer {dropLayerNumber}', values: { label, dropLabel, groupLabel, position, + dropLayerNumber, }, }), }; @@ -212,7 +236,7 @@ export const announcements: CustomAnnouncementsType = { field_combine: combineAnnouncement.selectedTarget, replace_compatible: replaceAnnouncement.selectedTarget, replace_incompatible: ( - { label, groupLabel, position }: HumanData, + { label, groupLabel, position, layerNumber }: HumanData, { label: dropLabel, groupLabel: dropGroupLabel, @@ -220,14 +244,16 @@ export const announcements: CustomAnnouncementsType = { nextLabel, canSwap, canDuplicate, + canCombine, + layerNumber: dropLayerNumber, }: HumanData, announceModifierKeys?: boolean ) => { - if (announceModifierKeys && (canSwap || canDuplicate)) { + if (announceModifierKeys && (canSwap || canDuplicate || canCombine)) { return i18n.translate( 'xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatibleMain', { - defaultMessage: `You're dragging {label} from {groupLabel} at position {position} over {dropLabel} from {dropGroupLabel} group at position {dropPosition}. Press space or enter to convert {label} to {nextLabel} and replace {dropLabel}.{duplicateCopy}{swapCopy}`, + defaultMessage: `You're dragging {label} from {groupLabel} at position {position} in layer {layerNumber} over {dropLabel} from {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Press space or enter to convert {label} to {nextLabel} and replace {dropLabel}.{duplicateCopy}{swapCopy}{combineCopy}`, values: { label, groupLabel, @@ -238,35 +264,40 @@ export const announcements: CustomAnnouncementsType = { nextLabel, duplicateCopy: canDuplicate ? DUPLICATE_SHORT : '', swapCopy: canSwap ? SWAP_SHORT : '', + combineCopy: canCombine ? COMBINE_SHORT : '', + layerNumber, + dropLayerNumber, }, } ); } return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatible', { - defaultMessage: `Convert {label} to {nextLabel} and replace {dropLabel} in {dropGroupLabel} group at position {dropPosition}. Press space or enter to replace`, + defaultMessage: `Convert {label} to {nextLabel} and replace {dropLabel} in {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Press space or enter to replace`, values: { label, - nextLabel, dropLabel, dropGroupLabel, dropPosition, + nextLabel, + dropLayerNumber, }, }); }, move_incompatible: ( - { label, groupLabel, position }: HumanData, + { label, groupLabel, position, layerNumber }: HumanData, { groupLabel: dropGroupLabel, position: dropPosition, nextLabel, canSwap, canDuplicate, + layerNumber: dropLayerNumber, }: HumanData, announceModifierKeys?: boolean ) => { if (announceModifierKeys && (canSwap || canDuplicate)) { return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.moveIncompatibleMain', { - defaultMessage: `You're dragging {label} from {groupLabel} at position {position} over position {dropPosition} in {dropGroupLabel} group. Press space or enter to convert {label} to {nextLabel} and move.{duplicateCopy}{swapCopy}`, + defaultMessage: `You're dragging {label} from {groupLabel} at position {position} in layer {layerNumber} over position {dropPosition} in {dropGroupLabel} group in layer {dropLayerNumber}. Press space or enter to convert {label} to {nextLabel} and move.{duplicateCopy}`, values: { label, groupLabel, @@ -275,29 +306,37 @@ export const announcements: CustomAnnouncementsType = { dropPosition, nextLabel, duplicateCopy: canDuplicate ? DUPLICATE_SHORT : '', - swapCopy: canSwap ? SWAP_SHORT : '', + layerNumber, + dropLayerNumber, }, }); } return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.moveIncompatible', { - defaultMessage: `Convert {label} to {nextLabel} and move to {dropGroupLabel} group at position {dropPosition}. Press space or enter to move`, + defaultMessage: `Convert {label} to {nextLabel} and move to {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Press space or enter to move`, values: { label, - nextLabel, dropGroupLabel, dropPosition, + nextLabel, + dropLayerNumber, }, }); }, move_compatible: ( { label, groupLabel, position }: HumanData, - { groupLabel: dropGroupLabel, position: dropPosition, canSwap, canDuplicate }: HumanData, + { + groupLabel: dropGroupLabel, + position: dropPosition, + canSwap, + canDuplicate, + layerNumber: dropLayerNumber, + }: HumanData, announceModifierKeys?: boolean ) => { if (announceModifierKeys && (canSwap || canDuplicate)) { return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.moveCompatibleMain', { - defaultMessage: `You're dragging {label} from {groupLabel} at position {position} over position {dropPosition} in {dropGroupLabel} group. Press space or enter to move.{duplicateCopy}{swapCopy}`, + defaultMessage: `You're dragging {label} from {groupLabel} at position {position} over position {dropPosition} in {dropGroupLabel} group in layer {dropLayerNumber}. Press space or enter to move.{duplicateCopy}`, values: { label, groupLabel, @@ -305,69 +344,78 @@ export const announcements: CustomAnnouncementsType = { dropGroupLabel, dropPosition, duplicateCopy: canDuplicate ? DUPLICATE_SHORT : '', - swapCopy: canSwap ? SWAP_SHORT : '', + dropLayerNumber, }, }); } return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.moveCompatible', { - defaultMessage: `Move {label} to {dropGroupLabel} group at position {dropPosition}. Press space or enter to move`, + defaultMessage: `Move {label} to {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Press space or enter to move`, values: { label, dropGroupLabel, dropPosition, + dropLayerNumber, }, }); }, duplicate_incompatible: ( { label }: HumanData, - { groupLabel, position, nextLabel }: HumanData + { groupLabel, position, nextLabel, layerNumber: dropLayerNumber }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.duplicateIncompatible', { defaultMessage: - 'Convert copy of {label} to {nextLabel} and add to {groupLabel} group at position {position}. Hold Alt or Option and press space or enter to duplicate', + 'Convert copy of {label} to {nextLabel} and add to {groupLabel} group at position {position} in layer {dropLayerNumber}. Hold Alt or Option and press space or enter to duplicate', values: { label, groupLabel, position, nextLabel, + dropLayerNumber, }, }), replace_duplicate_incompatible: ( { label }: HumanData, - { label: dropLabel, groupLabel, position, nextLabel }: HumanData + { label: dropLabel, groupLabel, position, nextLabel, layerNumber: dropLayerNumber }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replaceDuplicateIncompatible', { defaultMessage: - 'Convert copy of {label} to {nextLabel} and replace {dropLabel} in {groupLabel} group at position {position}. Hold Alt or Option and press space or enter to duplicate and replace', + 'Convert copy of {label} to {nextLabel} and replace {dropLabel} in {groupLabel} group at position {position} in layer {dropLayerNumber}. Hold Alt or Option and press space or enter to duplicate and replace', values: { label, groupLabel, position, dropLabel, nextLabel, + dropLayerNumber, }, }), replace_duplicate_compatible: ( { label }: HumanData, - { label: dropLabel, groupLabel, position }: HumanData + { label: dropLabel, groupLabel, position, layerNumber: dropLayerNumber }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replaceDuplicateCompatible', { defaultMessage: - 'Duplicate {label} and replace {dropLabel} in {groupLabel} at position {position}. Hold Alt or Option and press space or enter to duplicate and replace', + 'Duplicate {label} and replace {dropLabel} in {groupLabel} at position {position} in layer {dropLayerNumber}. Hold Alt or Option and press space or enter to duplicate and replace', values: { label, dropLabel, groupLabel, position, + dropLayerNumber, }, }), swap_compatible: ( - { label, groupLabel, position }: HumanData, - { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition }: HumanData + { label, groupLabel, position, layerNumber }: HumanData, + { + label: dropLabel, + groupLabel: dropGroupLabel, + position: dropPosition, + layerNumber: dropLayerNumber, + }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.swapCompatible', { defaultMessage: - 'Swap {label} in {groupLabel} group at position {position} with {dropLabel} in {dropGroupLabel} group at position {dropPosition}. Hold Shift and press space or enter to swap', + 'Swap {label} in {groupLabel} group at position {position} in layer {layerNumber} with {dropLabel} in {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Hold Shift and press space or enter to swap', values: { label, groupLabel, @@ -375,15 +423,23 @@ export const announcements: CustomAnnouncementsType = { dropLabel, dropGroupLabel, dropPosition, + layerNumber, + dropLayerNumber, }, }), swap_incompatible: ( - { label, groupLabel, position }: HumanData, - { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition, nextLabel }: HumanData + { label, groupLabel, position, layerNumber }: HumanData, + { + label: dropLabel, + groupLabel: dropGroupLabel, + position: dropPosition, + nextLabel, + layerNumber: dropLayerNumber, + }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.swapIncompatible', { defaultMessage: - 'Convert {label} to {nextLabel} in {groupLabel} group at position {position} and swap with {dropLabel} in {dropGroupLabel} group at position {dropPosition}. Hold Shift and press space or enter to swap', + 'Convert {label} to {nextLabel} in {groupLabel} group at position {position} in layer {layerNumber} and swap with {dropLabel} in {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Hold Shift and press space or enter to swap', values: { label, groupLabel, @@ -392,15 +448,22 @@ export const announcements: CustomAnnouncementsType = { dropGroupLabel, dropPosition, nextLabel, + layerNumber, + dropLayerNumber, }, }), combine_compatible: ( - { label, groupLabel, position }: HumanData, - { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition, nextLabel }: HumanData + { label, groupLabel, position, layerNumber }: HumanData, + { + label: dropLabel, + groupLabel: dropGroupLabel, + position: dropPosition, + layerNumber: dropLayerNumber, + }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.combineCompatible', { defaultMessage: - 'Combine {label} in {groupLabel} group at position {position} with {dropLabel} in {dropGroupLabel} group at position {dropPosition}. Hold Control and press space or enter to combine', + 'Combine {label} in {groupLabel} group at position {position} in layer {layerNumber} with {dropLabel} in {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Hold Control and press space or enter to combine', values: { label, groupLabel, @@ -408,15 +471,23 @@ export const announcements: CustomAnnouncementsType = { dropLabel, dropGroupLabel, dropPosition, + layerNumber, + dropLayerNumber, }, }), combine_incompatible: ( - { label, groupLabel, position }: HumanData, - { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition, nextLabel }: HumanData + { label, groupLabel, position, layerNumber }: HumanData, + { + label: dropLabel, + groupLabel: dropGroupLabel, + position: dropPosition, + nextLabel, + layerNumber: dropLayerNumber, + }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.combineIncompatible', { defaultMessage: - 'Convert {label} to {nextLabel} in {groupLabel} group at position {position} and combine with {dropLabel} in {dropGroupLabel} group at position {dropPosition}. Hold Control and press space or enter to combine', + 'Convert {label} to {nextLabel} in {groupLabel} group at position {position} in layer {layerNumber} and combine with {dropLabel} in {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}. Hold Control and press space or enter to combine', values: { label, groupLabel, @@ -425,6 +496,8 @@ export const announcements: CustomAnnouncementsType = { dropGroupLabel, dropPosition, nextLabel, + dropLayerNumber, + layerNumber, }, }), }, @@ -436,92 +509,110 @@ export const announcements: CustomAnnouncementsType = { replace_compatible: replaceAnnouncement.dropped, replace_incompatible: ( { label }: HumanData, - { label: dropLabel, groupLabel, position, nextLabel }: HumanData + { label: dropLabel, groupLabel, position, nextLabel, layerNumber: dropLayerNumber }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.dropped.replaceIncompatible', { defaultMessage: - 'Converted {label} to {nextLabel} and replaced {dropLabel} in {groupLabel} group at position {position}', + 'Converted {label} to {nextLabel} and replaced {dropLabel} in {groupLabel} group at position {position} in layer {dropLayerNumber}', values: { label, nextLabel, dropLabel, groupLabel, position, + dropLayerNumber, }, }), - move_incompatible: ({ label }: HumanData, { groupLabel, position, nextLabel }: HumanData) => + move_incompatible: ( + { label }: HumanData, + { groupLabel, position, nextLabel, layerNumber: dropLayerNumber }: HumanData + ) => i18n.translate('xpack.lens.dragDrop.announce.dropped.moveIncompatible', { defaultMessage: - 'Converted {label} to {nextLabel} and moved to {groupLabel} group at position {position}', + 'Converted {label} to {nextLabel} and moved to {groupLabel} group at position {position} in layer {dropLayerNumber}', values: { label, nextLabel, groupLabel, position, + dropLayerNumber, }, }), - move_compatible: ({ label }: HumanData, { groupLabel, position }: HumanData) => + move_compatible: ( + { label }: HumanData, + { groupLabel, position, layerNumber: dropLayerNumber }: HumanData + ) => i18n.translate('xpack.lens.dragDrop.announce.dropped.moveCompatible', { - defaultMessage: 'Moved {label} to {groupLabel} group at position {position}', + defaultMessage: + 'Moved {label} to {groupLabel} group at position {position} in layer {dropLayerNumber}', values: { label, groupLabel, position, + dropLayerNumber, }, }), duplicate_incompatible: ( { label }: HumanData, - { groupLabel, position, nextLabel }: HumanData + { groupLabel, position, nextLabel, layerNumber: dropLayerNumber }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.dropped.duplicateIncompatible', { defaultMessage: - 'Converted copy of {label} to {nextLabel} and added to {groupLabel} group at position {position}', + 'Converted copy of {label} to {nextLabel} and added to {groupLabel} group at position {position} in layer {dropLayerNumber}', values: { label, groupLabel, position, nextLabel, + dropLayerNumber, }, }), replace_duplicate_incompatible: ( { label }: HumanData, - { label: dropLabel, groupLabel, position, nextLabel }: HumanData + { label: dropLabel, groupLabel, position, nextLabel, layerNumber: dropLayerNumber }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.dropped.replaceDuplicateIncompatible', { defaultMessage: - 'Converted copy of {label} to {nextLabel} and replaced {dropLabel} in {groupLabel} group at position {position}', + 'Converted copy of {label} to {nextLabel} and replaced {dropLabel} in {groupLabel} group at position {position} in layer {dropLayerNumber}', values: { label, dropLabel, groupLabel, position, nextLabel, + dropLayerNumber, }, }), replace_duplicate_compatible: ( { label }: HumanData, - { label: dropLabel, groupLabel, position }: HumanData + { label: dropLabel, groupLabel, position, layerNumber: dropLayerNumber }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.duplicated.replaceDuplicateCompatible', { defaultMessage: - 'Replaced {dropLabel} with a copy of {label} in {groupLabel} at position {position}', + 'Replaced {dropLabel} with a copy of {label} in {groupLabel} at position {position} in layer {dropLayerNumber}', values: { label, dropLabel, groupLabel, position, + dropLayerNumber, }, }), swap_compatible: ( - { label, groupLabel, position }: HumanData, - { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition }: HumanData + { label, groupLabel, position, layerNumber }: HumanData, + { + label: dropLabel, + groupLabel: dropGroupLabel, + position: dropPosition, + layerNumber: dropLayerNumber, + }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.dropped.swapCompatible', { defaultMessage: - 'Moved {label} to {dropGroupLabel} at position {dropPosition} and {dropLabel} to {groupLabel} group at position {position}', + 'Moved {label} to {dropGroupLabel} at position {dropPosition} in layer {dropLayerNumber} and {dropLabel} to {groupLabel} group at position {position} in layer {layerNumber}', values: { label, groupLabel, @@ -529,15 +620,23 @@ export const announcements: CustomAnnouncementsType = { dropLabel, dropGroupLabel, dropPosition, + layerNumber, + dropLayerNumber, }, }), swap_incompatible: ( - { label, groupLabel, position }: HumanData, - { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition, nextLabel }: HumanData + { label, groupLabel, position, layerNumber }: HumanData, + { + label: dropLabel, + groupLabel: dropGroupLabel, + position: dropPosition, + nextLabel, + layerNumber: dropLayerNumber, + }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.dropped.swapIncompatible', { defaultMessage: - 'Converted {label} to {nextLabel} in {groupLabel} group at position {position} and swapped with {dropLabel} in {dropGroupLabel} group at position {dropPosition}', + 'Converted {label} to {nextLabel} in {groupLabel} group at position {position} in layer {layerNumber} and swapped with {dropLabel} in {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}', values: { label, groupLabel, @@ -546,31 +645,44 @@ export const announcements: CustomAnnouncementsType = { dropLabel, dropPosition, nextLabel, + dropLayerNumber, + layerNumber, }, }), combine_compatible: ( - { label, groupLabel, position }: HumanData, - { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition }: HumanData + { label, groupLabel }: HumanData, + { + label: dropLabel, + groupLabel: dropGroupLabel, + position: dropPosition, + layerNumber: dropLayerNumber, + }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.dropped.combineCompatible', { defaultMessage: - 'Combined {label} to {dropGroupLabel} at position {dropPosition} and {dropLabel} to {groupLabel} group at position {position}', + 'Combined {label} in group {groupLabel} to {dropLabel} in group {dropGroupLabel} at position {dropPosition} in layer {dropLayerNumber}', values: { label, groupLabel, - position, dropLabel, dropGroupLabel, dropPosition, + dropLayerNumber, }, }), combine_incompatible: ( - { label, groupLabel, position }: HumanData, - { label: dropLabel, groupLabel: dropGroupLabel, position: dropPosition, nextLabel }: HumanData + { label, groupLabel, position, layerNumber }: HumanData, + { + label: dropLabel, + groupLabel: dropGroupLabel, + position: dropPosition, + nextLabel, + layerNumber: dropLayerNumber, + }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.dropped.combineIncompatible', { defaultMessage: - 'Converted {label} to {nextLabel} in {groupLabel} group at position {position} and combined with {dropLabel} in {dropGroupLabel} group at position {dropPosition}', + 'Converted {label} to {nextLabel} in {groupLabel} group at position {position} and combined with {dropLabel} in {dropGroupLabel} group at position {dropPosition} in layer {dropLayerNumber}', values: { label, groupLabel, @@ -579,6 +691,7 @@ export const announcements: CustomAnnouncementsType = { dropLabel, dropPosition, nextLabel, + dropLayerNumber, }, }), }, @@ -620,15 +733,22 @@ const defaultAnnouncements = { dropped: ( { label }: HumanData, - { groupLabel: dropGroupLabel, position, label: dropLabel }: HumanData + { + groupLabel: dropGroupLabel, + position, + label: dropLabel, + layerNumber: dropLayerNumber, + }: HumanData ) => dropGroupLabel && position ? i18n.translate('xpack.lens.dragDrop.announce.droppedDefault', { - defaultMessage: 'Added {label} in {dropGroupLabel} group at position {position}', + defaultMessage: + 'Added {label} in {dropGroupLabel} group at position {position} in layer {dropLayerNumber}', values: { label, dropGroupLabel, position, + dropLayerNumber, }, }) : i18n.translate('xpack.lens.dragDrop.announce.droppedNoPosition', { @@ -640,15 +760,21 @@ const defaultAnnouncements = { }), selectedTarget: ( { label }: HumanData, - { label: dropLabel, groupLabel: dropGroupLabel, position }: HumanData + { + label: dropLabel, + groupLabel: dropGroupLabel, + position, + layerNumber: dropLayerNumber, + }: HumanData ) => { return dropGroupLabel && position ? i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.default', { - defaultMessage: `Add {label} to {dropGroupLabel} group at position {position}. Press space or enter to add`, + defaultMessage: `Add {label} to {dropGroupLabel} group at position {position} in layer {dropLayerNumber}. Press space or enter to add`, values: { label, dropGroupLabel, position, + dropLayerNumber, }, }) : i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.defaultNoPosition', { @@ -671,8 +797,15 @@ export const announce = { dropElement: HumanData, type?: DropType, announceModifierKeys?: boolean - ) => - (type && - announcements.selectedTarget?.[type]?.(draggedElement, dropElement, announceModifierKeys)) || - defaultAnnouncements.selectedTarget(draggedElement, dropElement), + ) => { + return ( + (type && + announcements.selectedTarget?.[type]?.( + draggedElement, + dropElement, + announceModifierKeys + )) || + defaultAnnouncements.selectedTarget(draggedElement, dropElement) + ); + }, }; diff --git a/x-pack/plugins/lens/public/drag_drop/providers/types.tsx b/x-pack/plugins/lens/public/drag_drop/providers/types.tsx index 921ab897706c0..363f0b41ef3a1 100644 --- a/x-pack/plugins/lens/public/drag_drop/providers/types.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers/types.tsx @@ -10,6 +10,7 @@ import { DropType } from '../../types'; export interface HumanData { label: string; groupLabel?: string; + layerNumber?: number; position?: number; nextLabel?: string; canSwap?: boolean; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx index f0e0911b708fd..32aba270e846b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/draggable_dimension_button.tsx @@ -10,10 +10,10 @@ import { DragDrop, DragDropIdentifier, DragContext } from '../../../../drag_drop import { Datasource, VisualizationDimensionGroupConfig, - isDraggedOperation, + isOperation, DropType, + DatasourceLayers, } from '../../../../types'; -import { LayerDatasourceDropProps } from '../types'; import { getCustomDropTarget, getAdditionalClassesOnDroppable, @@ -29,45 +29,53 @@ export function DraggableDimensionButton({ layerIndex, columnId, group, - groups, onDrop, onDragStart, onDragEnd, children, - layerDatasourceDropProps, + state, layerDatasource, + datasourceLayers, registerNewButtonRef, }: { layerId: string; groupIndex: number; layerIndex: number; - onDrop: ( - droppedItem: DragDropIdentifier, - dropTarget: DragDropIdentifier, - dropType?: DropType - ) => void; + onDrop: (source: DragDropIdentifier, dropTarget: DragDropIdentifier, dropType?: DropType) => void; onDragStart: () => void; onDragEnd: () => void; group: VisualizationDimensionGroupConfig; - groups: VisualizationDimensionGroupConfig[]; label: string; children: ReactElement; layerDatasource: Datasource; - layerDatasourceDropProps: LayerDatasourceDropProps; + datasourceLayers: DatasourceLayers; + state: unknown; accessorIndex: number; columnId: string; registerNewButtonRef: (id: string, instance: HTMLDivElement | null) => void; }) { const { dragging } = useContext(DragContext); - const dropProps = getDropProps(layerDatasource, { - ...(layerDatasourceDropProps || {}), - dragging, - columnId, - filterOperations: group.filterOperations, - groupId: group.groupId, - dimensionGroups: groups, - }); + const sharedDatasource = + !isOperation(dragging) || + datasourceLayers?.[dragging.layerId]?.datasourceId === datasourceLayers?.[layerId]?.datasourceId + ? layerDatasource + : undefined; + + const dropProps = getDropProps( + { + state, + source: dragging, + target: { + layerId, + columnId, + groupId: group.groupId, + filterOperations: group.filterOperations, + prioritizedOperation: group.prioritizedOperation, + }, + }, + sharedDatasource + ); const dropTypes = dropProps?.dropTypes; const nextLabel = dropProps?.nextLabel; @@ -104,6 +112,7 @@ export function DraggableDimensionButton({ groupLabel: group.groupLabel, position: accessorIndex + 1, nextLabel: nextLabel || '', + layerNumber: layerIndex + 1, }, }), [ @@ -118,10 +127,10 @@ export function DraggableDimensionButton({ canDuplicate, canSwap, canCombine, + layerIndex, ] ); - // todo: simplify by id and use drop targets? const reorderableGroup = useMemo( () => group.accessors.map((g) => ({ @@ -136,7 +145,7 @@ export function DraggableDimensionButton({ ); const handleOnDrop = useCallback( - (droppedItem, selectedDropType) => onDrop(droppedItem, value, selectedDropType), + (source, selectedDropType) => onDrop(source, value, selectedDropType), [value, onDrop] ); return ( @@ -151,7 +160,7 @@ export function DraggableDimensionButton({ getAdditionalClassesOnDroppable={getAdditionalClassesOnDroppable} order={[2, layerIndex, groupIndex, accessorIndex]} draggable - dragType={isDraggedOperation(dragging) ? 'move' : 'copy'} + dragType={isOperation(dragging) ? 'move' : 'copy'} dropTypes={dropTypes} reorderableGroup={reorderableGroup.length > 1 ? reorderableGroup : undefined} value={value} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.test.tsx new file mode 100644 index 0000000000000..dd5ec847fb5b5 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.test.tsx @@ -0,0 +1,123 @@ +/* + * 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 { getDropProps } from './drop_targets_utils'; +import { createMockDatasource } from '../../../../mocks'; + +describe('getDropProps', () => { + it('should run datasource getDropProps if exists', () => { + const mockDatasource = createMockDatasource('testDatasource'); + getDropProps( + { + state: 'datasourceState', + target: { + columnId: 'col1', + groupId: 'x', + layerId: 'first', + filterOperations: () => true, + }, + source: { + columnId: 'col1', + groupId: 'x', + layerId: 'first', + id: 'annotationColumn2', + humanData: { label: 'Event' }, + }, + }, + mockDatasource + ); + expect(mockDatasource.getDropProps).toHaveBeenCalled(); + }); + describe('no datasource', () => { + it('returns reorder for the same group existing columns', () => { + expect( + getDropProps({ + state: 'datasourceState', + target: { + columnId: 'annotationColumn', + groupId: 'xAnnotations', + layerId: 'second', + filterOperations: () => true, + }, + source: { + columnId: 'annotationColumn2', + groupId: 'xAnnotations', + layerId: 'second', + id: 'annotationColumn2', + humanData: { label: 'Event' }, + }, + }) + ).toEqual({ dropTypes: ['reorder'] }); + }); + it('returns duplicate for the same group existing column and not existing column', () => { + expect( + getDropProps({ + state: 'datasourceState', + target: { + columnId: 'annotationColumn', + groupId: 'xAnnotations', + layerId: 'second', + isNewColumn: true, + filterOperations: () => true, + }, + source: { + columnId: 'annotationColumn2', + groupId: 'xAnnotations', + layerId: 'second', + id: 'annotationColumn2', + humanData: { label: 'Event' }, + }, + }) + ).toEqual({ dropTypes: ['duplicate_compatible'] }); + }); + it('returns replace_duplicate and replace for replacing to different layer', () => { + expect( + getDropProps({ + state: 'datasourceState', + target: { + columnId: 'annotationColumn', + groupId: 'xAnnotations', + layerId: 'first', + filterOperations: () => true, + }, + source: { + columnId: 'annotationColumn2', + groupId: 'xAnnotations', + layerId: 'second', + id: 'annotationColumn2', + humanData: { label: 'Event' }, + }, + }) + ).toEqual({ + dropTypes: ['replace_compatible', 'replace_duplicate_compatible', 'swap_compatible'], + }); + }); + it('returns duplicate and move for replacing to different layer for empty column', () => { + expect( + getDropProps({ + state: 'datasourceState', + target: { + columnId: 'annotationColumn', + groupId: 'xAnnotations', + layerId: 'first', + isNewColumn: true, + filterOperations: () => true, + }, + source: { + columnId: 'annotationColumn2', + groupId: 'xAnnotations', + layerId: 'second', + id: 'annotationColumn2', + humanData: { label: 'Event' }, + }, + }) + ).toEqual({ + dropTypes: ['move_compatible', 'duplicate_compatible'], + }); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx index 056efbf379d8a..8ce2a4c0cc975 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx @@ -9,8 +9,17 @@ import React from 'react'; import classNames from 'classnames'; import { EuiIcon, EuiFlexItem, EuiFlexGroup, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DraggingIdentifier } from '../../../../drag_drop'; -import { Datasource, DropType, GetDropProps } from '../../../../types'; +import { DragDropIdentifier, DraggingIdentifier } from '../../../../drag_drop'; +import { + Datasource, + DropType, + FramePublicAPI, + GetDropPropsArgs, + isOperation, + Visualization, + DragDropOperation, + VisualizationDimensionGroupConfig, +} from '../../../../types'; function getPropsForDropType(type: 'swap' | 'duplicate' | 'combine') { switch (type) { @@ -131,35 +140,97 @@ export const getAdditionalClassesOnDroppable = (dropType?: string) => { } }; -const isOperationFromTheSameGroup = ( - op1?: DraggingIdentifier, - op2?: { layerId: string; groupId: string; columnId: string } -) => { +const isOperationFromCompatibleGroup = (op1?: DraggingIdentifier, op2?: DragDropOperation) => { return ( - op1 && - op2 && - 'columnId' in op1 && + isOperation(op1) && + isOperation(op2) && + op1.columnId !== op2.columnId && + op1.groupId === op2.groupId && + op1.layerId !== op2.layerId + ); +}; + +export const isOperationFromTheSameGroup = (op1?: DraggingIdentifier, op2?: DragDropOperation) => { + return ( + isOperation(op1) && + isOperation(op2) && op1.columnId !== op2.columnId && - 'groupId' in op1 && op1.groupId === op2.groupId && - 'layerId' in op1 && op1.layerId === op2.layerId ); }; +export function getDropPropsForSameGroup( + isNewColumn?: boolean +): { dropTypes: DropType[]; nextLabel?: string } | undefined { + return !isNewColumn ? { dropTypes: ['reorder'] } : { dropTypes: ['duplicate_compatible'] }; +} + export const getDropProps = ( - layerDatasource: Datasource, - dropProps: GetDropProps, - isNew?: boolean + dropProps: GetDropPropsArgs, + sharedDatasource?: Datasource ): { dropTypes: DropType[]; nextLabel?: string } | undefined => { - if (layerDatasource) { - return layerDatasource.getDropProps(dropProps); + if (sharedDatasource) { + return sharedDatasource?.getDropProps(dropProps); } else { - // TODO: refactor & test this - it's too annotations specific - // TODO: allow moving operations between layers for annotations - if (isOperationFromTheSameGroup(dropProps.dragging, dropProps)) { - return { dropTypes: [isNew ? 'duplicate_compatible' : 'reorder'], nextLabel: '' }; + if (isOperationFromTheSameGroup(dropProps.source, dropProps.target)) { + return getDropPropsForSameGroup(dropProps.target.isNewColumn); + } + if (isOperationFromCompatibleGroup(dropProps.source, dropProps.target)) { + return { + dropTypes: dropProps.target.isNewColumn + ? ['move_compatible', 'duplicate_compatible'] + : ['replace_compatible', 'replace_duplicate_compatible', 'swap_compatible'], + }; } } return; }; + +export interface OnVisDropProps { + prevState: T; + target: DragDropOperation; + source: DragDropIdentifier; + frame: FramePublicAPI; + dropType: DropType; + group?: VisualizationDimensionGroupConfig; +} + +export function onDropForVisualization( + props: OnVisDropProps, + activeVisualization: Visualization +) { + const { prevState, target, frame, dropType, source, group } = props; + const { layerId, columnId, groupId } = target; + + const previousColumn = + isOperation(source) && group?.requiresPreviousColumnOnDuplicate ? source.columnId : undefined; + + const newVisState = activeVisualization.setDimension({ + columnId, + groupId, + layerId, + prevState, + previousColumn, + frame, + }); + + // remove source + if ( + isOperation(source) && + (dropType === 'move_compatible' || + dropType === 'move_incompatible' || + dropType === 'combine_incompatible' || + dropType === 'combine_compatible' || + dropType === 'replace_compatible' || + dropType === 'replace_incompatible') + ) { + return activeVisualization.removeDimension({ + columnId: source?.columnId, + layerId: source?.layerId, + prevState: newVisState, + frame, + }); + } + return newVisState; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx index 867ce32ea700e..a35366611ae18 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx @@ -12,8 +12,13 @@ import { i18n } from '@kbn/i18n'; import { generateId } from '../../../../id_generator'; import { DragDrop, DragDropIdentifier, DragContext } from '../../../../drag_drop'; -import { Datasource, VisualizationDimensionGroupConfig, DropType } from '../../../../types'; -import { LayerDatasourceDropProps } from '../types'; +import { + Datasource, + VisualizationDimensionGroupConfig, + DropType, + DatasourceLayers, + isOperation, +} from '../../../../types'; import { getCustomDropTarget, getAdditionalClassesOnDroppable, @@ -98,31 +103,31 @@ const SuggestedValueButton = ({ columnId, group, onClick }: EmptyButtonProps) => export function EmptyDimensionButton({ group, - groups, layerDatasource, - layerDatasourceDropProps, + state, layerId, groupIndex, layerIndex, onClick, onDrop, + datasourceLayers, }: { layerId: string; groupIndex: number; layerIndex: number; - onDrop: ( - droppedItem: DragDropIdentifier, - dropTarget: DragDropIdentifier, - dropType?: DropType - ) => void; + onDrop: (source: DragDropIdentifier, dropTarget: DragDropIdentifier, dropType?: DropType) => void; onClick: (id: string) => void; group: VisualizationDimensionGroupConfig; - groups: VisualizationDimensionGroupConfig[]; - layerDatasource: Datasource; - layerDatasourceDropProps: LayerDatasourceDropProps; + datasourceLayers: DatasourceLayers; + state: unknown; }) { const { dragging } = useContext(DragContext); + const sharedDatasource = + !isOperation(dragging) || + datasourceLayers?.[dragging.layerId]?.datasourceId === datasourceLayers?.[layerId]?.datasourceId + ? layerDatasource + : undefined; const itemIndex = group.accessors.length; @@ -132,16 +137,19 @@ export function EmptyDimensionButton({ }, [itemIndex]); const dropProps = getDropProps( - layerDatasource, { - ...(layerDatasourceDropProps || {}), - dragging, - columnId: newColumnId, - filterOperations: group.filterOperations, - groupId: group.groupId, - dimensionGroups: groups, + state, + source: dragging, + target: { + layerId, + columnId: newColumnId, + groupId: group.groupId, + filterOperations: group.filterOperations, + prioritizedOperation: group.prioritizedOperation, + isNewColumn: true, + }, }, - true + sharedDatasource ); const dropTypes = dropProps?.dropTypes; @@ -157,6 +165,7 @@ export function EmptyDimensionButton({ columnId: newColumnId, groupId: group.groupId, layerId, + filterOperations: group.filterOperations, id: newColumnId, humanData: { label, @@ -164,13 +173,24 @@ export function EmptyDimensionButton({ position: itemIndex + 1, nextLabel: nextLabel || '', canDuplicate, + layerNumber: layerIndex + 1, }, }), - [newColumnId, group.groupId, layerId, group.groupLabel, itemIndex, nextLabel, canDuplicate] + [ + newColumnId, + group.groupId, + layerId, + group.groupLabel, + group.filterOperations, + itemIndex, + nextLabel, + canDuplicate, + layerIndex, + ] ); const handleOnDrop = React.useCallback( - (droppedItem, selectedDropType) => onDrop(droppedItem, value, selectedDropType), + (source, selectedDropType) => onDrop(source, value, selectedDropType), [value, onDrop] ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index e5da3b0feef03..02c5f1c23967f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -631,7 +631,7 @@ describe('LayerPanel', () => { expect(mockDatasource.getDropProps).toHaveBeenCalledWith( expect.objectContaining({ - dragging: draggingField, + source: draggingField, }) ); @@ -644,7 +644,7 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ - droppedItem: draggingField, + source: draggingField, }) ); }); @@ -663,8 +663,8 @@ describe('LayerPanel', () => { ], }); - mockDatasource.getDropProps.mockImplementation(({ columnId }) => - columnId !== 'a' ? { dropTypes: ['field_replace'], nextLabel: '' } : undefined + mockDatasource.getDropProps.mockImplementation(({ target }) => + target.columnId !== 'a' ? { dropTypes: ['field_replace'], nextLabel: '' } : undefined ); const { instance } = await mountWithProvider( @@ -674,7 +674,9 @@ describe('LayerPanel', () => { ); expect(mockDatasource.getDropProps).toHaveBeenCalledWith( - expect.objectContaining({ columnId: 'a' }) + expect.objectContaining({ + target: expect.objectContaining({ columnId: 'a', groupId: 'a', layerId: 'first' }), + }) ); expect( @@ -741,7 +743,7 @@ describe('LayerPanel', () => { expect(mockDatasource.getDropProps).toHaveBeenCalledWith( expect.objectContaining({ - dragging: draggingOperation, + source: draggingOperation, }) ); @@ -755,8 +757,8 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ - columnId: 'b', - droppedItem: draggingOperation, + target: expect.objectContaining({ columnId: 'b' }), + source: draggingOperation, }) ); @@ -771,8 +773,8 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ - columnId: 'newid', - droppedItem: draggingOperation, + target: expect.objectContaining({ columnId: 'newid' }), + source: draggingOperation, }) ); }); @@ -816,7 +818,7 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ dropType: 'reorder', - droppedItem: draggingOperation, + source: draggingOperation, }) ); const secondButton = instance @@ -865,9 +867,9 @@ describe('LayerPanel', () => { }); expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ - columnId: 'newid', + target: expect.objectContaining({ columnId: 'newid' }), dropType: 'duplicate_compatible', - droppedItem: draggingOperation, + source: draggingOperation, }) ); }); @@ -907,7 +909,7 @@ describe('LayerPanel', () => { humanData: { label: 'Label' }, }; - mockDatasource.onDrop.mockReturnValue({ deleted: 'a' }); + mockDatasource.onDrop.mockReturnValue(true); const updateVisualization = jest.fn(); const { instance } = await mountWithProvider( @@ -925,9 +927,10 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ dropType: 'replace_compatible', - droppedItem: draggingOperation, + source: draggingOperation, }) ); + // testing default onDropForVisualization path expect(mockVis.setDimension).toHaveBeenCalledWith( expect.objectContaining({ columnId: 'c', @@ -945,6 +948,85 @@ describe('LayerPanel', () => { ); expect(updateVisualization).toHaveBeenCalledTimes(1); }); + it('should call onDrop and update visualization when replacing between compatible groups2', async () => { + const mockVis = { + ...mockVisualization, + removeDimension: jest.fn(), + setDimension: jest.fn(() => 'modifiedState'), + onDrop: jest.fn(() => 'modifiedState'), + }; + jest.spyOn(mockVis.onDrop, 'bind').mockImplementation((thisVal, ...args) => mockVis.onDrop); + + mockVis.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [{ columnId: 'a' }, { columnId: 'b' }], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + { + groupLabel: 'B', + groupId: 'b', + accessors: [{ columnId: 'c' }], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup2', + }, + ], + }); + + const draggingOperation = { + layerId: 'first', + columnId: 'a', + groupId: 'a', + id: 'a', + humanData: { label: 'Label' }, + }; + + mockDatasource.onDrop.mockReturnValue(true); + const updateVisualization = jest.fn(); + + const { instance } = await mountWithProvider( + + + + ); + act(() => { + instance.find(DragDrop).at(3).prop('onDrop')!(draggingOperation, 'replace_compatible'); + }); + + expect(mockDatasource.onDrop).toHaveBeenCalledWith( + expect.objectContaining({ + dropType: 'replace_compatible', + source: draggingOperation, + }) + ); + + expect(mockVis.onDrop).toHaveBeenCalledWith( + expect.objectContaining({ + dropType: 'replace_compatible', + prevState: 'state', + source: draggingOperation, + target: expect.objectContaining({ + columnId: 'c', + groupId: 'b', + id: 'c', + layerId: 'first', + }), + }), + mockVis + ); + expect(mockVis.setDimension).not.toHaveBeenCalled(); + expect(mockVis.removeDimension).not.toHaveBeenCalled(); + expect(updateVisualization).toHaveBeenCalledTimes(1); + }); }); describe('add a new dimension', () => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index c577bf89d6bd1..0c54ca0df5c71 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -19,7 +19,13 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NativeRenderer } from '../../../native_renderer'; -import { StateSetter, Visualization, DraggedOperation, DropType } from '../../../types'; +import { + StateSetter, + Visualization, + DragDropOperation, + DropType, + isOperation, +} from '../../../types'; import { DragDropIdentifier, ReorderProvider } from '../../../drag_drop'; import { LayerSettings } from './layer_settings'; import { trackUiEvent } from '../../../lens_ui_telemetry'; @@ -36,6 +42,7 @@ import { selectResolvedDateRange, selectDatasourceStates, } from '../../../state_management'; +import { onDropForVisualization } from './buttons/drop_targets_utils'; const initialActiveDimensionState = { isNew: false, @@ -109,19 +116,12 @@ export function LayerPanel( const layerDatasourceState = datasourceStates?.[datasourceId]?.state; const layerDatasource = props.datasourceMap[datasourceId]; - const layerDatasourceDropProps = useMemo( - () => ({ - layerId, - state: layerDatasourceState, - setState: (newState: unknown) => { - updateDatasource(datasourceId, newState); - }, - }), - [layerId, layerDatasourceState, datasourceId, updateDatasource] - ); - const layerDatasourceConfigProps = { - ...layerDatasourceDropProps, + state: layerDatasourceState, + setState: (newState: unknown) => { + updateDatasource(datasourceId, newState); + }, + layerId, frame: props.framePublicAPI, dateRange, }; @@ -155,105 +155,70 @@ export function LayerPanel( registerNewRef: registerNewButtonRef, } = useFocusUpdate(allAccessors); - const layerDatasourceOnDrop = layerDatasource?.onDrop; - const onDrop = useMemo(() => { - return ( - droppedItem: DragDropIdentifier, - targetItem: DragDropIdentifier, - dropType?: DropType - ) => { + return (source: DragDropIdentifier, target: DragDropIdentifier, dropType?: DropType) => { if (!dropType) { return; } - const { - columnId, - groupId, - layerId: targetLayerId, - } = targetItem as unknown as DraggedOperation; + if (!isOperation(target)) { + throw new Error('Drop target should be an operation'); + } + if (dropType === 'reorder' || dropType === 'field_replace' || dropType === 'field_add') { - setNextFocusedButtonId(droppedItem.id); + setNextFocusedButtonId(source.id); } else { - setNextFocusedButtonId(columnId); + setNextFocusedButtonId(target.columnId); } + let hasDropSucceeded = true; if (layerDatasource) { - const group = groups.find(({ groupId: gId }) => gId === groupId); - const filterOperations = group?.filterOperations || (() => false); - const dropResult = layerDatasourceOnDrop({ - ...layerDatasourceDropProps, - droppedItem, - columnId, - layerId: targetLayerId, - filterOperations, - dimensionGroups: groups, - groupId, - dropType, - }); - if (dropResult) { - let previousColumn = - typeof droppedItem.column === 'string' ? droppedItem.column : undefined; - - // make it inherit only for moving and duplicate - if (!previousColumn) { - // when duplicating check if the previous column is required - if ( - dropType === 'duplicate_compatible' && - typeof droppedItem.columnId === 'string' && - group?.requiresPreviousColumnOnDuplicate - ) { - previousColumn = droppedItem.columnId; - } else { - previousColumn = typeof dropResult === 'object' ? dropResult.deleted : undefined; - } - } - const newVisState = activeVisualization.setDimension({ - columnId, - groupId, - layerId: targetLayerId, - prevState: props.visualizationState, - previousColumn, - frame: framePublicAPI, - }); + hasDropSucceeded = Boolean( + layerDatasource?.onDrop({ + state: layerDatasourceState, + setState: (newState: unknown) => { + updateDatasource(datasourceId, newState); + }, + source, + target: { + ...(target as unknown as DragDropOperation), + filterOperations: + groups.find(({ groupId: gId }) => gId === target.groupId)?.filterOperations || + Boolean, + }, + dimensionGroups: groups, + dropType, + }) + ); + } + if (hasDropSucceeded) { + activeVisualization.onDrop = activeVisualization.onDrop?.bind(activeVisualization); - if (typeof dropResult === 'object') { - // When a column is moved, we delete the reference to the old - updateVisualization( - activeVisualization.removeDimension({ - columnId: dropResult.deleted, - layerId: targetLayerId, - prevState: newVisState, - frame: framePublicAPI, - }) - ); - } else { - updateVisualization(newVisState); - } - } - } else { - if (dropType === 'duplicate_compatible' || dropType === 'reorder') { - const newVisState = activeVisualization.setDimension({ - columnId, - groupId, - layerId: targetLayerId, - prevState: props.visualizationState, - previousColumn: droppedItem.id, - frame: framePublicAPI, - }); - updateVisualization(newVisState); - } + updateVisualization( + (activeVisualization.onDrop || onDropForVisualization)?.( + { + prevState: props.visualizationState, + frame: framePublicAPI, + target, + source, + dropType, + group: groups.find(({ groupId: gId }) => gId === target.groupId), + }, + activeVisualization + ) + ); } }; }, [ layerDatasource, + layerDatasourceState, setNextFocusedButtonId, groups, - layerDatasourceOnDrop, - layerDatasourceDropProps, activeVisualization, props.visualizationState, framePublicAPI, updateVisualization, + datasourceId, + updateDatasource, ]); const isDimensionPanelOpen = Boolean(activeId); @@ -462,15 +427,15 @@ export function LayerPanel( return ( setHideTooltip(true)} @@ -562,12 +527,12 @@ export function LayerPanel( {group.supportsMoreColumns ? ( { props.onEmptyDimensionAdd(id, group); setActiveDimension({ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts index 66a30b0a405e8..172e0702f56e8 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts @@ -29,7 +29,6 @@ export interface LayerPanelProps { } export interface LayerDatasourceDropProps { - layerId: string; state: unknown; setState: (newState: unknown) => void; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts deleted file mode 100644 index 66714f494bf53..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts +++ /dev/null @@ -1,2332 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; -import { IndexPatternDimensionEditorProps } from '../dimension_panel'; -import { onDrop } from './on_drop_handler'; -import { getDropProps } from './get_drop_props'; -import { - IUiSettingsClient, - SavedObjectsClientContract, - HttpSetup, - CoreSetup, -} from '@kbn/core/public'; -import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; -import { IndexPatternLayer, IndexPatternPrivateState } from '../../types'; -import { documentField } from '../../document_field'; -import { OperationMetadata, DropType } from '../../../types'; -import { - DateHistogramIndexPatternColumn, - GenericIndexPatternColumn, - MedianIndexPatternColumn, - TermsIndexPatternColumn, -} from '../../operations'; -import { getFieldByNameFactory } from '../../pure_helpers'; -import { generateId } from '../../../id_generator'; -import { layerTypes } from '../../../../common'; - -jest.mock('../../../id_generator'); - -const fields = [ - { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'bytes', - displayName: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'memory', - displayName: 'memory', - type: 'number', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'source', - displayName: 'source', - type: 'string', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'src', - displayName: 'src', - type: 'string', - aggregatable: true, - searchable: true, - exists: true, - }, - { - name: 'dest', - displayName: 'dest', - type: 'string', - aggregatable: true, - searchable: true, - exists: true, - }, - documentField, -]; - -const expectedIndexPatterns = { - foo: { - id: 'foo', - title: 'my-fake-index-pattern', - timeFieldName: 'timestamp', - hasExistence: true, - hasRestrictions: false, - fields, - getFieldByName: getFieldByNameFactory(fields), - }, -}; - -const dimensionGroups = [ - { - accessors: [], - groupId: 'a', - supportsMoreColumns: true, - hideGrouping: true, - groupLabel: '', - filterOperations: (op: OperationMetadata) => op.isBucketed, - }, - { - accessors: [{ columnId: 'col1' }, { columnId: 'col2' }, { columnId: 'col3' }], - groupId: 'b', - supportsMoreColumns: true, - hideGrouping: true, - groupLabel: '', - filterOperations: (op: OperationMetadata) => op.isBucketed, - }, - { - accessors: [{ columnId: 'col4' }], - groupId: 'c', - supportsMoreColumns: true, - hideGrouping: true, - groupLabel: '', - filterOperations: (op: OperationMetadata) => op.isBucketed === false, - }, -]; - -const oneColumnLayer: IndexPatternLayer = { - indexPatternId: 'foo', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Date histogram of timestamp', - customLabel: true, - dataType: 'date', - isBucketed: true, - - // Private - operationType: 'date_histogram', - params: { - interval: '1d', - }, - sourceField: 'timestamp', - } as DateHistogramIndexPatternColumn, - }, - incompleteColumns: {}, -}; - -const multipleColumnsLayer: IndexPatternLayer = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3', 'col4'], - columns: { - col1: oneColumnLayer.columns.col1, - col2: { - label: 'Top 10 values of src', - dataType: 'string', - isBucketed: true, - // Private - operationType: 'terms', - params: { - orderBy: { type: 'alphabetical' }, - orderDirection: 'desc', - size: 10, - }, - sourceField: 'src', - } as TermsIndexPatternColumn, - col3: { - label: 'Top 10 values of dest', - dataType: 'string', - isBucketed: true, - - // Private - operationType: 'terms', - params: { - orderBy: { type: 'alphabetical' }, - orderDirection: 'desc', - size: 10, - }, - sourceField: 'dest', - } as TermsIndexPatternColumn, - col4: { - label: 'Median of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'median', - sourceField: 'bytes', - }, - }, -}; - -const draggingField = { - field: { type: 'number', name: 'bytes', aggregatable: true }, - indexPatternId: 'foo', - id: 'bar', - humanData: { label: 'Label' }, -}; - -const draggingCol1 = { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'col1', - humanData: { label: 'Column 1' }, -}; - -const draggingCol2 = { - columnId: 'col2', - groupId: 'b', - layerId: 'first', - id: 'col2', - humanData: { label: 'Column 2' }, - filterOperations: (op: OperationMetadata) => op.isBucketed, -}; - -const draggingCol3 = { - columnId: 'col3', - groupId: 'b', - layerId: 'first', - id: 'col3', - humanData: { - label: '', - }, -}; - -const draggingCol4 = { - columnId: 'col4', - groupId: 'c', - layerId: 'first', - id: 'col4', - humanData: { - label: '', - }, - filterOperations: (op: OperationMetadata) => op.isBucketed === false, -}; - -/** - * The datasource exposes four main pieces of code which are tested at - * an integration test level. The main reason for this fairly high level - * of testing is that there is a lot of UI logic that isn't easily - * unit tested, such as the transient invalid state. - * - * - Dimension trigger: Not tested here - * - Dimension editor component: First half of the tests - * - * - getDropProps: Returns drop types that are possible for the current dragging field or other dimension - * - onDrop: Correct application of drop logic - */ -describe('IndexPatternDimensionEditorPanel', () => { - let state: IndexPatternPrivateState; - let setState: jest.Mock; - let defaultProps: IndexPatternDimensionEditorProps; - - function getStateWithMultiFieldColumn() { - return { - ...state, - layers: { - ...state.layers, - first: { - ...state.layers.first, - columns: { - ...state.layers.first.columns, - col1: { - label: 'Top values of dest', - dataType: 'string', - isBucketed: true, - - // Private - operationType: 'terms', - params: { - orderBy: { type: 'alphabetical' }, - orderDirection: 'desc', - size: 10, - }, - sourceField: 'dest', - } as TermsIndexPatternColumn, - }, - }, - }, - }; - } - - beforeEach(() => { - state = { - indexPatternRefs: [], - indexPatterns: expectedIndexPatterns, - currentIndexPatternId: 'foo', - isFirstExistenceFetch: false, - existingFields: { - 'my-fake-index-pattern': { - timestamp: true, - bytes: true, - memory: true, - source: true, - }, - }, - layers: { first: { ...oneColumnLayer } }, - }; - - setState = jest.fn(); - - defaultProps = { - state, - setState, - dateRange: { fromDate: 'now-1d', toDate: 'now' }, - columnId: 'col1', - layerId: 'first', - uniqueLabel: 'stuff', - groupId: 'group1', - filterOperations: () => true, - storage: {} as IStorageWrapper, - uiSettings: {} as IUiSettingsClient, - savedObjectsClient: {} as SavedObjectsClientContract, - http: {} as HttpSetup, - data: { - fieldFormats: { - getType: jest.fn().mockReturnValue({ - id: 'number', - title: 'Number', - }), - getDefaultType: jest.fn().mockReturnValue({ - id: 'bytes', - title: 'Bytes', - }), - } as unknown as DataPublicPluginStart['fieldFormats'], - } as unknown as DataPublicPluginStart, - unifiedSearch: {} as UnifiedSearchPublicPluginStart, - dataViews: {} as DataViewsPublicPluginStart, - core: {} as CoreSetup, - dimensionGroups: [], - isFullscreen: false, - toggleFullscreen: () => {}, - supportStaticValue: false, - layerType: layerTypes.DATA, - }; - - jest.clearAllMocks(); - }); - - const groupId = 'a'; - - describe('getDropProps', () => { - it('returns undefined if no drag is happening', () => { - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { name: 'bar', id: 'bar', humanData: { label: 'Label' } }, - }) - ).toBe(undefined); - }); - - it('returns undefined if the dragged item has no field', () => { - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { - name: 'bar', - id: 'bar', - humanData: { label: 'Label' }, - }, - }) - ).toBe(undefined); - }); - - describe('dragging a field', () => { - it('returns undefined if field is not supported by filterOperations', () => { - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: draggingField, - filterOperations: () => false, - }) - ).toBe(undefined); - }); - - it('returns field_replace if the field is supported by filterOperations and the dropTarget is an existing column', () => { - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: draggingField, - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - }) - ).toEqual({ dropTypes: ['field_replace'], nextLabel: 'Intervals' }); - }); - - it('returns field_add if the field is supported by filterOperations and the dropTarget is an empty column', () => { - expect( - getDropProps({ - ...defaultProps, - columnId: 'newId', - groupId, - dragging: draggingField, - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - }) - ).toEqual({ dropTypes: ['field_add'], nextLabel: 'Intervals' }); - }); - - it('returns undefined if the field belongs to another index pattern', () => { - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { - field: { type: 'number', name: 'bar', aggregatable: true }, - indexPatternId: 'foo2', - id: 'bar', - humanData: { label: 'Label' }, - }, - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - }) - ).toBe(undefined); - }); - - it('returns undefined if the dragged field is already in use by this operation', () => { - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { - field: { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - exists: true, - }, - indexPatternId: 'foo', - id: 'bar', - humanData: { label: 'Label' }, - }, - }) - ).toBe(undefined); - }); - - it('returns also field_combine if the field is supported by filterOperations and the dropTarget is an existing column that supports multiple fields', () => { - // replace the state with a top values column to enable the multi fields behaviour - state = getStateWithMultiFieldColumn(); - expect( - getDropProps({ - ...defaultProps, - state, - groupId, - dragging: draggingField, - filterOperations: (op: OperationMetadata) => op.dataType !== 'date', - }) - ).toEqual({ dropTypes: ['field_replace', 'field_combine'] }); - }); - }); - - describe('dragging a column', () => { - it('returns undefined if the dragged column from different group uses the same field as the dropTarget', () => { - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: state.layers.first.columns.col1, - - col2: { - label: 'Date histogram of timestamp (1)', - customLabel: true, - dataType: 'date', - isBucketed: true, - - // Private - operationType: 'date_histogram', - params: { - interval: '1d', - }, - sourceField: 'timestamp', - } as DateHistogramIndexPatternColumn, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { - ...draggingCol1, - groupId: 'c', - }, - columnId: 'col2', - }) - ).toEqual(undefined); - }); - - it('returns undefined if the dragged column from different group uses the same fields as the dropTarget', () => { - state = getStateWithMultiFieldColumn(); - const sourceMultiFieldColumn = { - ...state.layers.first.columns.col1, - sourceField: 'bytes', - params: { - ...(state.layers.first.columns.col1 as TermsIndexPatternColumn).params, - secondaryFields: ['dest'], - }, - } as TermsIndexPatternColumn; - // invert the fields - const targetMultiFieldColumn = { - ...state.layers.first.columns.col1, - sourceField: 'dest', - params: { - ...(state.layers.first.columns.col1 as TermsIndexPatternColumn).params, - secondaryFields: ['bytes'], - }, - } as TermsIndexPatternColumn; - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2'], - columns: { - col1: sourceMultiFieldColumn, - col2: targetMultiFieldColumn, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - state, - groupId, - dragging: { - ...draggingCol1, - groupId: 'c', - }, - columnId: 'col2', - }) - ).toEqual(undefined); - }); - - it('returns duplicate and replace if the dragged column from different group uses the same field as the dropTarget, but this last one is multifield, and can be swappable', () => { - state = getStateWithMultiFieldColumn(); - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2'], - columns: { - col1: state.layers.first.columns.col1, - - col2: { - ...state.layers.first.columns.col1, - sourceField: 'bytes', - params: { - ...(state.layers.first.columns.col1 as TermsIndexPatternColumn).params, - secondaryFields: ['dest'], - }, - } as TermsIndexPatternColumn, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - state, - groupId, - dragging: { - ...draggingCol1, - groupId: 'c', - }, - columnId: 'col2', - }) - ).toEqual({ - dropTypes: ['replace_compatible', 'replace_duplicate_compatible'], - }); - }); - - it('returns swap, duplicate and replace if the dragged column from different group uses the same field as the dropTarget, but this last one is multifield', () => { - state = getStateWithMultiFieldColumn(); - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2'], - columns: { - col1: state.layers.first.columns.col1, - - col2: { - ...state.layers.first.columns.col1, - sourceField: 'bytes', - params: { - ...(state.layers.first.columns.col1 as TermsIndexPatternColumn).params, - secondaryFields: ['dest'], - }, - } as TermsIndexPatternColumn, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - state, - // make it swappable - dimensionGroups: [ - { - accessors: [{ columnId: 'col1' }], - filterOperations: jest.fn(() => true), - groupId, - groupLabel: '', - supportsMoreColumns: false, - }, - ], - groupId, - dragging: { - ...draggingCol1, - groupId: 'c', - }, - columnId: 'col2', - }) - ).toEqual({ - dropTypes: ['replace_compatible', 'replace_duplicate_compatible', 'swap_compatible'], - }); - }); - - it('returns reorder if drop target and droppedItem columns are from the same group and both are existing', () => { - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: state.layers.first.columns.col1, - - col2: { - label: 'Sum of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'sum', - sourceField: 'bytes', - }, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { ...draggingCol1, groupId }, - columnId: 'col2', - filterOperations: (op: OperationMetadata) => op.isBucketed === false, - }) - ).toEqual({ - dropTypes: ['reorder'], - }); - }); - - it('returns duplicate_compatible if drop target and droppedItem columns are from the same group and drop target id is a new column', () => { - expect( - getDropProps({ - ...defaultProps, - columnId: 'newId', - groupId, - dragging: { - ...draggingCol1, - groupId, - }, - }) - ).toEqual({ dropTypes: ['duplicate_compatible'] }); - }); - - it('returns compatible drop types if the dragged column is compatible', () => { - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { - ...draggingCol1, - groupId: 'c', - }, - columnId: 'col2', - }) - ).toEqual({ dropTypes: ['move_compatible', 'duplicate_compatible'] }); - }); - - it('returns incompatible drop target types if dropping column to existing incompatible column', () => { - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: state.layers.first.columns.col1, - - col2: { - label: 'Sum of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'sum', - sourceField: 'bytes', - }, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { - ...draggingCol1, - groupId: 'c', - }, - columnId: 'col2', - filterOperations: (op: OperationMetadata) => op.isBucketed === false, - }) - ).toEqual({ - dropTypes: [ - 'replace_incompatible', - 'replace_duplicate_incompatible', - 'swap_incompatible', - ], - nextLabel: 'Minimum', - }); - }); - - it('does not return swap_incompatible if current dropTarget column cannot be swapped to the group of dragging column', () => { - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: state.layers.first.columns.col1, - - col2: { - label: 'Count of records', - dataType: 'number', - isBucketed: false, - sourceField: '___records___', - operationType: 'count', - }, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - groupId, - dragging: { - columnId: 'col1', - groupId: 'b', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - filterOperations: (op: OperationMetadata) => op.isBucketed === true, - }, - columnId: 'col2', - filterOperations: (op: OperationMetadata) => op.isBucketed === false, - }) - ).toEqual({ - dropTypes: ['replace_incompatible', 'replace_duplicate_incompatible'], - nextLabel: 'Minimum', - }); - }); - - it('returns combine_compatible drop type if the dragged column is compatible and the target one support multiple fields', () => { - state = getStateWithMultiFieldColumn(); - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2'], - columns: { - col1: state.layers.first.columns.col1, - - col2: { - ...state.layers.first.columns.col1, - sourceField: 'bytes', - }, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - state, - groupId, - dragging: { - ...draggingCol1, - groupId: 'c', - }, - columnId: 'col2', - }) - ).toEqual({ - dropTypes: ['replace_compatible', 'replace_duplicate_compatible', 'combine_compatible'], - }); - }); - - it('returns no combine_compatible drop type if the target column uses rarity ordering', () => { - state = getStateWithMultiFieldColumn(); - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2'], - columns: { - col1: state.layers.first.columns.col1, - - col2: { - ...state.layers.first.columns.col1, - sourceField: 'bytes', - params: { - ...(state.layers.first.columns.col1 as TermsIndexPatternColumn).params, - orderBy: { type: 'rare' }, - }, - } as TermsIndexPatternColumn, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - state, - groupId, - dragging: { - ...draggingCol1, - groupId: 'c', - }, - columnId: 'col2', - }) - ).toEqual({ - dropTypes: ['replace_compatible', 'replace_duplicate_compatible'], - }); - }); - - it('returns no combine drop type if the dragged column is compatible, the target one supports multiple fields but there are too many fields', () => { - state = getStateWithMultiFieldColumn(); - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2'], - columns: { - col1: state.layers.first.columns.col1, - - col2: { - ...state.layers.first.columns.col1, - sourceField: 'source', - params: { - ...(state.layers.first.columns.col1 as TermsIndexPatternColumn).params, - secondaryFields: ['memory', 'bytes', 'geo.src'], // too many fields here - }, - } as TermsIndexPatternColumn, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - state, - groupId, - dragging: { - ...draggingCol1, - groupId: 'c', - }, - columnId: 'col2', - }) - ).toEqual({ - dropTypes: ['replace_compatible', 'replace_duplicate_compatible'], - }); - }); - - it('returns combine_incompatible drop target types if dropping column to existing incompatible column which supports multiple fields', () => { - state = getStateWithMultiFieldColumn(); - state.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: state.layers.first.columns.col1, - - col2: { - label: 'Sum of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'sum', - sourceField: 'bytes', - }, - }, - }; - - expect( - getDropProps({ - ...defaultProps, - state, - groupId, - // drag the sum over the top values - dragging: { - ...draggingCol2, - groupId: 'c', - filterOperation: undefined, - }, - columnId: 'col1', - filterOperations: (op: OperationMetadata) => op.isBucketed, - }) - ).toEqual({ - dropTypes: [ - 'replace_incompatible', - 'replace_duplicate_incompatible', - 'swap_incompatible', - 'combine_incompatible', - ], - nextLabel: 'Top values', - }); - }); - }); - }); - - describe('onDrop', () => { - describe('dropping a field', () => { - it('updates a column when a field is dropped', () => { - onDrop({ - ...defaultProps, - droppedItem: draggingField, - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - dropType: 'field_replace', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: expect.objectContaining({ - columns: expect.objectContaining({ - col1: expect.objectContaining({ - dataType: 'number', - sourceField: 'bytes', - }), - }), - }), - }, - }); - }); - it('selects the specific operation that was valid on drop', () => { - onDrop({ - ...defaultProps, - droppedItem: draggingField, - columnId: 'col2', - filterOperations: (op: OperationMetadata) => op.isBucketed, - dropType: 'field_replace', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columnOrder: ['col1', 'col2'], - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - dataType: 'number', - sourceField: 'bytes', - }), - }, - }, - }, - }); - }); - it('keeps the operation when dropping a different compatible field', () => { - onDrop({ - ...defaultProps, - droppedItem: { - field: { name: 'memory', type: 'number', aggregatable: true }, - indexPatternId: 'foo', - id: '1', - }, - state: { - ...state, - layers: { - first: { - indexPatternId: 'foo', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Sum of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'sum', - sourceField: 'bytes', - }, - }, - }, - }, - }, - dropType: 'field_replace', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: expect.objectContaining({ - columns: expect.objectContaining({ - col1: expect.objectContaining({ - operationType: 'sum', - dataType: 'number', - sourceField: 'memory', - }), - }), - }), - }, - }); - }); - it('appends the dropped column when a field is dropped', () => { - onDrop({ - ...defaultProps, - droppedItem: draggingField, - dropType: 'field_replace', - columnId: 'col2', - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columnOrder: ['col1', 'col2'], - columns: { - ...state.layers.first.columns, - col2: expect.objectContaining({ - dataType: 'number', - sourceField: 'bytes', - }), - }, - }, - }, - }); - }); - it('dimensionGroups are defined - appends the dropped column in the right place when a field is dropped', () => { - const testState = { ...state }; - testState.layers.first = { ...multipleColumnsLayer }; - // config: - // a: - // b: col1, col2, col3 - // c: col4 - // dragging field into newCol in group a - - onDrop({ - ...defaultProps, - droppedItem: draggingField, - columnId: 'newCol', - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - groupId: 'a', - dimensionGroups, - dropType: 'field_add', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], - columns: { - newCol: expect.objectContaining({ - dataType: 'number', - sourceField: 'bytes', - }), - col1: testState.layers.first.columns.col1, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - incompleteColumns: {}, - }, - }, - }); - }); - - it('appends the new field to the column that supports multiple fields when a field is dropped', () => { - state = getStateWithMultiFieldColumn(); - onDrop({ - ...defaultProps, - state, - droppedItem: draggingField, - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - dropType: 'field_combine', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: expect.objectContaining({ - columns: expect.objectContaining({ - col1: expect.objectContaining({ - dataType: 'string', - sourceField: 'dest', - params: expect.objectContaining({ secondaryFields: ['bytes'] }), - }), - }), - }), - }, - }); - }); - }); - - describe('dropping a dimension', () => { - const dragging = { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }; - - it('sets correct order in group for metric and bucket columns when duplicating a column in group', () => { - const testState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: { - label: 'Date histogram of timestamp', - dataType: 'date', - isBucketed: true, - operationType: 'date_histogram', - params: { - interval: '1d', - }, - sourceField: 'timestamp', - } as DateHistogramIndexPatternColumn, - col2: { - label: 'Top values of bar', - dataType: 'number', - isBucketed: true, - operationType: 'terms', - sourceField: 'bar', - params: { - orderBy: { type: 'alphabetical' }, - orderDirection: 'asc', - size: 5, - }, - } as TermsIndexPatternColumn, - col3: { - operationType: 'average', - sourceField: 'memory', - label: 'average of memory', - dataType: 'number', - isBucketed: false, - }, - }, - }, - }, - }; - - const referenceDragging = { - columnId: 'col3', - groupId: 'a', - layerId: 'first', - id: 'col3', - humanData: { label: 'Label' }, - }; - - onDrop({ - ...defaultProps, - droppedItem: referenceDragging, - state: testState, - dropType: 'duplicate_compatible', - columnId: 'newCol', - }); - // metric is appended - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'col3', 'newCol'], - columns: { - col1: testState.layers.first.columns.col1, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - newCol: testState.layers.first.columns.col3, - }, - }, - }, - }); - - const bucketDragging = { - columnId: 'col2', - groupId: 'a', - layerId: 'first', - id: 'col2', - humanData: { label: 'Label' }, - }; - - onDrop({ - ...defaultProps, - droppedItem: bucketDragging, - state: testState, - dropType: 'duplicate_compatible', - columnId: 'newCol', - }); - - // bucket is placed after the last existing bucket - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'newCol', 'col3'], - columns: { - col1: testState.layers.first.columns.col1, - col2: testState.layers.first.columns.col2, - newCol: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - }, - }, - }, - }); - }); - - it('when duplicating fullReference column, the referenced columns get duplicated too', () => { - (generateId as jest.Mock).mockReturnValue(`ref1Copy`); - const testState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1', 'ref1'], - columns: { - col1: { - label: 'Test reference', - dataType: 'number', - isBucketed: false, - operationType: 'cumulative_sum', - references: ['ref1'], - }, - ref1: { - label: 'Count of records', - dataType: 'number', - isBucketed: false, - sourceField: '___records___', - operationType: 'count', - }, - }, - }, - }, - }; - const referenceDragging = { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }; - onDrop({ - ...defaultProps, - droppedItem: referenceDragging, - state: testState, - dropType: 'duplicate_compatible', - columnId: 'col1Copy', - }); - - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'ref1', 'ref1Copy', 'col1Copy'], - columns: { - ref1: testState.layers.first.columns.ref1, - col1: testState.layers.first.columns.col1, - ref1Copy: { ...testState.layers.first.columns.ref1 }, - col1Copy: { - ...testState.layers.first.columns.col1, - references: ['ref1Copy'], - }, - }, - }, - }, - }); - }); - - it('when duplicating fullReference column, the multiple referenced columns get duplicated too', () => { - (generateId as jest.Mock).mockReturnValueOnce(`ref1Copy`); - (generateId as jest.Mock).mockReturnValueOnce(`ref2Copy`); - const testState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1', 'ref1'], - columns: { - col1: { - label: 'Test reference', - dataType: 'number', - isBucketed: false, - operationType: 'cumulative_sum', - references: ['ref1', 'ref2'], - }, - ref1: { - label: 'Count of records', - dataType: 'number', - isBucketed: false, - sourceField: '___records___', - operationType: 'count', - }, - ref2: { - label: 'Unique count of bytes', - dataType: 'number', - isBucketed: false, - sourceField: 'bytes', - operationType: 'unique_count', - }, - }, - }, - }, - }; - const metricDragging = { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }; - onDrop({ - ...defaultProps, - droppedItem: metricDragging, - state: testState, - dropType: 'duplicate_compatible', - columnId: 'col1Copy', - }); - - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'ref1', 'ref2', 'ref1Copy', 'col1Copy', 'ref2Copy'], - columns: { - ref1: testState.layers.first.columns.ref1, - ref2: testState.layers.first.columns.ref2, - col1: testState.layers.first.columns.col1, - ref2Copy: { ...testState.layers.first.columns.ref2 }, - ref1Copy: { ...testState.layers.first.columns.ref1 }, - col1Copy: { - ...testState.layers.first.columns.col1, - references: ['ref1Copy', 'ref2Copy'], - }, - }, - }, - }, - }); - }); - - it('when duplicating fullReference column, the referenced columns get duplicated recursively', () => { - (generateId as jest.Mock).mockReturnValueOnce(`ref1Copy`); - (generateId as jest.Mock).mockReturnValueOnce(`innerRef1Copy`); - (generateId as jest.Mock).mockReturnValueOnce(`ref2Copy`); - const testState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['innerRef1', 'ref2', 'ref1', 'col1'], - columns: { - col1: { - label: 'Test reference', - dataType: 'number', - isBucketed: false, - operationType: 'cumulative_sum', - references: ['ref1', 'ref2'], - }, - ref1: { - label: 'Reference that has a reference', - dataType: 'number', - isBucketed: false, - operationType: 'cumulative_sum', - references: ['innerRef1'], - }, - innerRef1: { - label: 'Count of records', - dataType: 'number', - isBucketed: false, - sourceField: '___records___', - operationType: 'count', - }, - ref2: { - label: 'Unique count of bytes', - dataType: 'number', - isBucketed: false, - sourceField: 'bytes', - operationType: 'unique_count', - }, - }, - }, - }, - }; - const refDragging = { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }; - onDrop({ - ...defaultProps, - droppedItem: refDragging, - state: testState, - dropType: 'duplicate_compatible', - columnId: 'col1Copy', - }); - - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: [ - 'innerRef1', - 'ref2', - 'ref1', - 'col1', - 'innerRef1Copy', - 'ref1Copy', - 'col1Copy', - 'ref2Copy', - ], - columns: { - innerRef1: testState.layers.first.columns.innerRef1, - ref1: testState.layers.first.columns.ref1, - ref2: testState.layers.first.columns.ref2, - col1: testState.layers.first.columns.col1, - - innerRef1Copy: { ...testState.layers.first.columns.innerRef1 }, - ref2Copy: { ...testState.layers.first.columns.ref2 }, - ref1Copy: { - ...testState.layers.first.columns.ref1, - references: ['innerRef1Copy'], - }, - col1Copy: { - ...testState.layers.first.columns.col1, - references: ['ref1Copy', 'ref2Copy'], - }, - }, - }, - }, - }); - }); - - it('when duplicating fullReference column onto exisitng column, the state will not get modified', () => { - (generateId as jest.Mock).mockReturnValue(`ref1Copy`); - const testState: IndexPatternPrivateState = { - ...state, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col2', 'ref1', 'col1'], - columns: { - col1: { - label: 'Test reference', - dataType: 'number', - isBucketed: false, - operationType: 'cumulative_sum', - references: ['ref1'], - }, - ref1: { - label: 'Count of records', - dataType: 'number', - isBucketed: false, - sourceField: '___records___', - operationType: 'count', - }, - col2: { - label: 'Minimum', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'min', - sourceField: 'bytes', - customLabel: true, - }, - }, - }, - }, - }; - const referenceDragging = { - columnId: 'col1', - groupId: 'a', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }; - onDrop({ - ...defaultProps, - droppedItem: referenceDragging, - state: testState, - dropType: 'duplicate_compatible', - columnId: 'col2', - }); - - expect(setState).toHaveBeenCalledWith(testState); - }); - - it('sets correct order in group when reordering a column in group', () => { - const testState = { - ...state, - layers: { - first: { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: { - label: 'Date histogram of timestamp', - dataType: 'date', - isBucketed: true, - } as GenericIndexPatternColumn, - col2: { - label: 'Top values of bar', - dataType: 'number', - isBucketed: true, - } as GenericIndexPatternColumn, - col3: { - label: 'Top values of memory', - dataType: 'number', - isBucketed: true, - } as GenericIndexPatternColumn, - }, - }, - }, - }; - - const defaultReorderDropParams = { - ...defaultProps, - dragging, - droppedItem: draggingCol1, - state: testState, - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - dropType: 'reorder' as DropType, - }; - - const stateWithColumnOrder = (columnOrder: string[]) => { - return { - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder, - columns: { - ...testState.layers.first.columns, - }, - }, - }, - }; - }; - - // first element to last - onDrop({ - ...defaultReorderDropParams, - columnId: 'col3', - }); - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col2', 'col3', 'col1'])); - - // last element to first - onDrop({ - ...defaultReorderDropParams, - columnId: 'col1', - droppedItem: { - columnId: 'col3', - groupId: 'a', - layerId: 'first', - id: 'col3', - }, - }); - expect(setState).toBeCalledTimes(2); - expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col3', 'col1', 'col2'])); - - // middle column to first - onDrop({ - ...defaultReorderDropParams, - columnId: 'col1', - droppedItem: { - columnId: 'col2', - groupId: 'a', - layerId: 'first', - id: 'col2', - }, - }); - expect(setState).toBeCalledTimes(3); - expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col2', 'col1', 'col3'])); - - // middle column to last - onDrop({ - ...defaultReorderDropParams, - columnId: 'col3', - droppedItem: { - columnId: 'col2', - groupId: 'a', - layerId: 'first', - id: 'col2', - }, - }); - expect(setState).toBeCalledTimes(4); - expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col1', 'col3', 'col2'])); - }); - - it('updates the column id when moving an operation to an empty dimension', () => { - onDrop({ - ...defaultProps, - droppedItem: draggingCol1, - columnId: 'col2', - dropType: 'move_compatible', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...state.layers.first, - columnOrder: ['col2'], - columns: { - col2: state.layers.first.columns.col1, - }, - }, - }, - }); - }); - - it('replaces an operation when moving to a populated dimension', () => { - const testState = { ...state }; - testState.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: testState.layers.first.columns.col1, - - col2: { - label: 'Top 10 values of src', - dataType: 'string', - isBucketed: true, - - // Private - operationType: 'terms', - params: { - orderBy: { type: 'column', columnId: 'col3' }, - orderDirection: 'desc', - size: 10, - }, - sourceField: 'src', - } as TermsIndexPatternColumn, - col3: { - label: 'Count', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'count', - sourceField: '___records___', - customLabel: true, - }, - }, - }; - - onDrop({ - ...defaultProps, - droppedItem: draggingCol2, - state: testState, - dropType: 'replace_compatible', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col3'], - columns: { - col1: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - }, - }, - }, - }); - }); - - it('when combine compatible columns should append dropped column fields into the target one', () => { - state = getStateWithMultiFieldColumn(); - state.layers.first.columns = { - ...state.layers.first.columns, - col2: { - isBucketed: true, - label: 'Top values of source', - operationType: 'terms', - sourceField: 'bytes', - dataType: 'number', - params: { - orderBy: { - type: 'alphabetical', - }, - orderDirection: 'desc', - size: 10, - }, - } as TermsIndexPatternColumn, - }; - onDrop({ - ...defaultProps, - state, - droppedItem: { - columnId: 'col2', - groupId: 'a', - layerId: 'first', - id: 'col2', - humanData: { label: 'Label' }, - }, - filterOperations: (op: OperationMetadata) => op.isBucketed, - dropType: 'combine_compatible', - columnId: 'col1', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: expect.objectContaining({ - columns: expect.objectContaining({ - col1: expect.objectContaining({ - dataType: 'string', - sourceField: 'dest', - params: expect.objectContaining({ secondaryFields: ['bytes'] }), - }), - }), - }), - }, - }); - }); - - describe('dimension group aware ordering and copying', () => { - let testState: IndexPatternPrivateState; - beforeEach(() => { - testState = { ...state }; - testState.layers.first = { ...multipleColumnsLayer }; - }); - - it('respects groups on moving operations between compatible groups', () => { - // config: - // a: - // b: col1, col2, col3 - // c: col4 - // dragging col2 into newCol in group a - onDrop({ - ...defaultProps, - columnId: 'newCol', - droppedItem: draggingCol2, - state: testState, - groupId: 'a', - dimensionGroups, - dropType: 'move_compatible', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['newCol', 'col1', 'col3', 'col4'], - columns: { - newCol: testState.layers.first.columns.col2, - col1: testState.layers.first.columns.col1, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - }, - }, - }); - }); - - it('respects groups on duplicating operations between compatible groups', () => { - // config: - // a: - // b: col1, col2, col3 - // c: col4 - // dragging col2 into newCol in group a - onDrop({ - ...defaultProps, - columnId: 'newCol', - droppedItem: draggingCol2, - state: testState, - groupId: 'a', - dimensionGroups, - dropType: 'duplicate_compatible', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], - columns: { - newCol: testState.layers.first.columns.col2, - col1: testState.layers.first.columns.col1, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - }, - }, - }); - }); - - it('respects groups on moving operations between compatible groups with overwrite', () => { - // config: - // a: col1, - // b: col2, col3 - // c: col4 - // dragging col3 onto col1 in group a - onDrop({ - ...defaultProps, - columnId: 'col1', - droppedItem: draggingCol3, - state: testState, - groupId: 'a', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - dropType: 'move_compatible', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'col4'], - columns: { - col1: testState.layers.first.columns.col3, - col2: testState.layers.first.columns.col2, - col4: testState.layers.first.columns.col4, - }, - }, - }, - }); - }); - - it('respects groups on moving operations if some columns are not listed in groups', () => { - // config: - // a: col1, - // b: col2, col3 - // c: col4 - // col5, col6 not in visualization groups - // dragging col3 onto col1 in group a - onDrop({ - ...defaultProps, - columnId: 'col1', - droppedItem: draggingCol3, - state: { - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'col3', 'col4', 'col5', 'col6'], - columns: { - ...testState.layers.first.columns, - col5: { - dataType: 'number', - operationType: 'count', - label: '', - isBucketed: false, - sourceField: '___records___', - customLabel: true, - }, - col6: { - dataType: 'number', - operationType: 'count', - label: '', - isBucketed: false, - sourceField: '___records___', - customLabel: true, - }, - }, - }, - }, - }, - groupId: 'a', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - dropType: 'move_compatible', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'col4', 'col5', 'col6'], - columns: { - col1: testState.layers.first.columns.col3, - col2: testState.layers.first.columns.col2, - col4: testState.layers.first.columns.col4, - col5: expect.objectContaining({ - dataType: 'number', - operationType: 'count', - label: '', - isBucketed: false, - sourceField: '___records___', - }), - col6: expect.objectContaining({ - dataType: 'number', - operationType: 'count', - label: '', - isBucketed: false, - sourceField: '___records___', - }), - }, - }, - }, - }); - }); - - it('respects groups on duplicating operations between compatible groups with overwrite', () => { - // config: - // a: col1, - // b: col2, col3 - // c: col4 - // dragging col3 onto col1 in group a - - onDrop({ - ...defaultProps, - columnId: 'col1', - droppedItem: draggingCol3, - state: testState, - groupId: 'a', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - dropType: 'duplicate_compatible', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'col3', 'col4'], - columns: { - col1: testState.layers.first.columns.col3, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - }, - }, - }); - }); - - it('moves newly created dimension to the bottom of the current group', () => { - // config: - // a: col1 - // b: col2, col3 - // c: col4 - // dragging col1 into newCol in group b - onDrop({ - ...defaultProps, - columnId: 'newCol', - dropType: 'move_compatible', - droppedItem: draggingCol1, - state: testState, - groupId: 'b', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col2', 'col3', 'newCol', 'col4'], - columns: { - newCol: testState.layers.first.columns.col1, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - }, - }, - }); - }); - - it('copies column to the bottom of the current group', () => { - // config: - // a: col1 - // b: col2, col3 - // c: col4 - // copying col1 within group a - onDrop({ - ...defaultProps, - columnId: 'newCol', - dropType: 'duplicate_compatible', - droppedItem: draggingCol1, - state: testState, - groupId: 'a', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'newCol', 'col2', 'col3', 'col4'], - columns: { - col1: testState.layers.first.columns.col1, - newCol: testState.layers.first.columns.col1, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - }, - }, - }); - }); - - it('appends the dropped column in the right place respecting custom nestingOrder', () => { - // config: - // a: - // b: col1, col2, col3 - // c: col4 - // dragging field into newCol in group a - - onDrop({ - ...defaultProps, - droppedItem: draggingField, - columnId: 'newCol', - filterOperations: (op: OperationMetadata) => op.dataType === 'number', - groupId: 'a', - dimensionGroups: [ - // a and b are ordered in reverse visually, but nesting order keeps them in place for column order - { ...dimensionGroups[1], nestingOrder: 1 }, - { ...dimensionGroups[0], nestingOrder: 0 }, - { ...dimensionGroups[2] }, - ], - dropType: 'field_add', - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], - columns: { - newCol: expect.objectContaining({ - dataType: 'number', - sourceField: 'bytes', - }), - col1: testState.layers.first.columns.col1, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - incompleteColumns: {}, - }, - }, - }); - }); - - it('moves incompatible column to the bottom of the target group', () => { - // config: - // a: col1 - // b: col2, col3 - // c: col4 - // dragging col4 into newCol in group a - - onDrop({ - ...defaultProps, - columnId: 'newCol', - dropType: 'move_incompatible', - droppedItem: draggingCol4, - state: testState, - groupId: 'a', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'newCol', 'col2', 'col3'], - columns: { - col1: testState.layers.first.columns.col1, - newCol: expect.objectContaining({ - sourceField: (testState.layers.first.columns.col4 as MedianIndexPatternColumn) - .sourceField, - }), - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - }, - incompleteColumns: {}, - }, - }, - }); - }); - - it('copies incompatible column to the bottom of the target group', () => { - // config: - // a: col1 - // b: col2, col3 - // c: col4 - // dragging col4 into newCol in group a - - onDrop({ - ...defaultProps, - columnId: 'newCol', - dropType: 'duplicate_incompatible', - droppedItem: draggingCol4, - state: testState, - groupId: 'a', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'newCol', 'col2', 'col3', 'col4'], - columns: { - col1: testState.layers.first.columns.col1, - newCol: expect.objectContaining({ - sourceField: (testState.layers.first.columns.col4 as MedianIndexPatternColumn) - .sourceField, - }), - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col4, - }, - incompleteColumns: {}, - }, - }, - }); - }); - - it('moves incompatible column with overwrite keeping order of target column', () => { - // config: - // a: col1 - // b: col2, col3 - // c: col4 - // dragging col4 into col2 in group b - - onDrop({ - ...defaultProps, - columnId: 'col2', - dropType: 'move_incompatible', - droppedItem: draggingCol4, - state: testState, - groupId: 'b', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: testState.layers.first.columns.col1, - col2: { - isBucketed: true, - label: 'Top 10 values of bytes', - operationType: 'terms', - sourceField: 'bytes', - dataType: 'number', - params: { - orderBy: { - type: 'alphabetical', - }, - orderDirection: 'desc', - size: 10, - parentFormat: { id: 'terms' }, - }, - }, - col3: testState.layers.first.columns.col3, - }, - incompleteColumns: {}, - }, - }, - }); - }); - - it('when swapping compatibly, columns carry order', () => { - // config: - // a: col1 - // b: col2, col3 - // c: col4 - // dragging col4 into col1 - - onDrop({ - ...defaultProps, - columnId: 'col1', - dropType: 'swap_compatible', - droppedItem: draggingCol4, - state: testState, - groupId: 'a', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'col3', 'col4'], - columns: { - col1: testState.layers.first.columns.col4, - col2: testState.layers.first.columns.col2, - col3: testState.layers.first.columns.col3, - col4: testState.layers.first.columns.col1, - }, - }, - }, - }); - }); - - it('when swapping incompatibly, newly created columns take order from the columns they replace', () => { - // config: - // a: col1 - // b: col2, col3 - // c: col4 - // dragging col4 into col2 - - onDrop({ - ...defaultProps, - columnId: 'col2', - dropType: 'swap_incompatible', - droppedItem: draggingCol4, - state: testState, - groupId: 'b', - dimensionGroups: [ - { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, - { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, - { ...dimensionGroups[2] }, - ], - }); - - expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith({ - ...testState, - layers: { - first: { - ...testState.layers.first, - columnOrder: ['col1', 'col2', 'col3', 'col4'], - columns: { - col1: testState.layers.first.columns.col1, - col2: { - isBucketed: true, - label: 'Top 10 values of bytes', - operationType: 'terms', - sourceField: 'bytes', - dataType: 'number', - params: { - orderBy: { - type: 'alphabetical', - }, - orderDirection: 'desc', - parentFormat: { id: 'terms' }, - size: 10, - }, - }, - col3: testState.layers.first.columns.col3, - col4: { - isBucketed: false, - label: 'Unique count of src', - filter: undefined, - operationType: 'unique_count', - sourceField: 'src', - timeShift: undefined, - dataType: 'number', - params: { - emptyAsNull: true, - }, - scale: 'ratio', - }, - }, - incompleteColumns: {}, - }, - }, - }); - }); - }); - }); - }); -}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.test.ts new file mode 100644 index 0000000000000..f9afc9a00c98f --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.test.ts @@ -0,0 +1,767 @@ +/* + * 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 { DragDropOperation, OperationMetadata } from '../../../types'; +import { TermsIndexPatternColumn } from '../../operations'; +import { getDropProps } from './get_drop_props'; +import { + mockDataViews, + mockedLayers, + mockedDraggedField, + mockedDndOperations, + mockedColumns, +} from './mocks'; +import { generateId } from '../../../id_generator'; + +const getDefaultProps = () => ({ + state: { + indexPatternRefs: [], + indexPatterns: mockDataViews(), + currentIndexPatternId: 'first', + isFirstExistenceFetch: false, + existingFields: { + first: { + timestamp: true, + bytes: true, + memory: true, + source: true, + }, + }, + layers: { first: mockedLayers.doubleColumnLayer(), second: mockedLayers.emptyLayer() }, + }, + target: mockedDndOperations.notFiltering, + source: mockedDndOperations.bucket, +}); + +describe('IndexPatternDimensionEditorPanel#getDropProps', () => { + describe('not dragging', () => { + it('returns undefined if no drag is happening', () => { + expect(getDropProps({ ...getDefaultProps(), source: undefined })).toBe(undefined); + }); + + it('returns undefined if the dragged item has no field', () => { + expect( + getDropProps({ + ...getDefaultProps(), + source: { name: 'bar', id: 'bar', humanData: { label: 'Label' } }, + }) + ).toBe(undefined); + }); + }); + + describe('dragging a field', () => { + it('returns undefined if field is not supported by filterOperations', () => { + expect( + getDropProps({ + ...getDefaultProps(), + source: mockedDraggedField, + target: mockedDndOperations.staticValue, + }) + ).toBe(undefined); + }); + + it('returns field_replace if the field is supported by filterOperations and the dropTarget is an existing column', () => { + expect( + getDropProps({ + ...getDefaultProps(), + target: mockedDndOperations.numericalOnly, + source: mockedDraggedField, + }) + ).toEqual({ dropTypes: ['field_replace'], nextLabel: 'Intervals' }); + }); + + it('returns field_add if the field is supported by filterOperations and the dropTarget is an empty column', () => { + expect( + getDropProps({ + ...getDefaultProps(), + target: { + ...mockedDndOperations.numericalOnly, + columnId: 'newId', + }, + source: mockedDraggedField, + }) + ).toEqual({ dropTypes: ['field_add'], nextLabel: 'Intervals' }); + }); + + it('returns undefined if the field belongs to another data view', () => { + expect( + getDropProps({ + ...getDefaultProps(), + source: { + ...mockedDraggedField, + indexPatternId: 'first2', + }, + }) + ).toBe(undefined); + }); + + it('returns undefined if the dragged field is already in use by this operation', () => { + expect( + getDropProps({ + ...getDefaultProps(), + source: { + ...mockedDraggedField, + field: { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + exists: true, + }, + }, + }) + ).toBe(undefined); + }); + + it('returns also field_combine if the field is supported by filterOperations and the dropTarget is an existing column that supports multiple fields', () => { + // replace the state with a top values column to enable the multi fields behaviour + const props = getDefaultProps(); + expect( + getDropProps({ + ...props, + source: mockedDraggedField, + target: { + ...props.target, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.dataType !== 'date', + }, + }) + ).toEqual({ dropTypes: ['field_replace', 'field_combine'] }); + }); + }); + + describe('dragging a column', () => { + it('allows replacing and replace-duplicating when two columns from compatible groups use the same field', () => { + const props = getDefaultProps(); + props.state.layers.first.columns.col2 = mockedColumns.dateHistogramCopy; + + expect( + getDropProps({ + ...props, + target: { + ...props.target, + columnId: 'col2', + }, + source: { + ...mockedDndOperations.metric, + groupId: 'c', + }, + }) + ).toEqual({ dropTypes: ['replace_compatible', 'replace_duplicate_compatible'] }); + }); + + it('returns correct dropTypes if the dragged column from different group uses the same fields as the dropTarget', () => { + const props = getDefaultProps(); + const sourceMultiFieldColumn = { + ...props.state.layers.first.columns.col1, + sourceField: 'bytes', + params: { + ...(props.state.layers.first.columns.col1 as TermsIndexPatternColumn).params, + secondaryFields: ['dest'], + }, + } as TermsIndexPatternColumn; + // invert the fields + const targetMultiFieldColumn = { + ...props.state.layers.first.columns.col1, + sourceField: 'dest', + params: { + ...(props.state.layers.first.columns.col1 as TermsIndexPatternColumn).params, + secondaryFields: ['bytes'], + }, + } as TermsIndexPatternColumn; + props.state.layers.first.columns = { + col1: sourceMultiFieldColumn, + col2: targetMultiFieldColumn, + }; + + expect( + getDropProps({ + ...props, + target: { + ...props.target, + columnId: 'col2', + }, + source: { + ...mockedDndOperations.metric, + groupId: 'c', + }, + }) + ).toEqual({ dropTypes: ['replace_compatible', 'replace_duplicate_compatible'] }); + }); + + it('returns duplicate and replace if the dragged column from different group uses the same field as the dropTarget, but this last one is multifield, and can be swappable', () => { + const props = getDefaultProps(); + props.state.layers.first.columns.col2 = { + ...props.state.layers.first.columns.col1, + sourceField: 'bytes', + params: { + ...(props.state.layers.first.columns.col1 as TermsIndexPatternColumn).params, + secondaryFields: ['dest'], + }, + } as TermsIndexPatternColumn; + + expect( + getDropProps({ + ...props, + target: { + ...props.target, + columnId: 'col2', + }, + source: { + ...mockedDndOperations.metric, + groupId: 'c', + }, + }) + ).toEqual({ + dropTypes: ['replace_compatible', 'replace_duplicate_compatible'], + }); + }); + + it('returns swap, duplicate and replace if the dragged column from different group uses the same field as the dropTarget, but this last one is multifield', () => { + const props = getDefaultProps(); + props.state.layers.first.columns.col2 = { + ...props.state.layers.first.columns.col1, + sourceField: 'bytes', + params: { + ...(props.state.layers.first.columns.col1 as TermsIndexPatternColumn).params, + secondaryFields: ['dest'], + }, + } as TermsIndexPatternColumn; + + expect( + getDropProps({ + ...props, + ...props, + // make it swappable + target: { + ...props.target, + filterOperations: (op: OperationMetadata) => op.isBucketed, + groupId: 'a', + columnId: 'col2', + }, + source: { + ...mockedDndOperations.metric, + filterOperations: (op: OperationMetadata) => op.isBucketed, + groupId: 'c', + }, + }) + ).toEqual({ + dropTypes: ['replace_compatible', 'replace_duplicate_compatible', 'swap_compatible'], + }); + }); + + it('returns reorder if drop target and source columns are from the same group and both are existing', () => { + const props = getDefaultProps(); + props.state.layers.first.columns.col2 = mockedColumns.sum; + + expect( + getDropProps({ + ...props, + source: { ...mockedDndOperations.metric, groupId: 'a' }, + target: { + ...props.target, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.isBucketed === false, + }, + }) + ).toEqual({ + dropTypes: ['reorder'], + }); + }); + + it('returns duplicate_compatible if drop target and source columns are from the same group and drop target id is a new column', () => { + const props = getDefaultProps(); + expect( + getDropProps({ + ...props, + target: { + ...props.target, + groupId: 'a', + columnId: 'newId', + }, + source: { + ...mockedDndOperations.metric, + groupId: 'a', + }, + }) + ).toEqual({ dropTypes: ['duplicate_compatible'] }); + }); + + it('returns compatible drop types if the dragged column is compatible', () => { + const props = getDefaultProps(); + expect( + getDropProps({ + ...props, + target: { + ...props.target, + groupId: 'a', + columnId: 'col3', + }, + source: { + ...mockedDndOperations.metric, + groupId: 'c', + }, + }) + ).toEqual({ dropTypes: ['move_compatible', 'duplicate_compatible'] }); + }); + + it('returns incompatible drop target types if dropping column to existing incompatible column', () => { + const props = getDefaultProps(); + props.state.layers.first.columns = { + col1: mockedColumns.dateHistogram, + col2: mockedColumns.sum, + }; + + expect( + getDropProps({ + ...props, + target: { + ...props.target, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.isBucketed === false, + }, + source: { + ...mockedDndOperations.metric, + groupId: 'c', + }, + }) + ).toEqual({ + dropTypes: ['replace_incompatible', 'replace_duplicate_incompatible', 'swap_incompatible'], + nextLabel: 'Minimum', + }); + }); + + it('does not return swap_incompatible if current dropTarget column cannot be swapped to the group of dragging column', () => { + const props = getDefaultProps(); + props.state.layers.first.columns = { + col1: mockedColumns.dateHistogram, + col2: mockedColumns.count, + }; + + expect( + getDropProps({ + ...props, + target: { + ...props.target, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.isBucketed === false, + }, + source: { + columnId: 'col1', + groupId: 'b', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, + filterOperations: (op: OperationMetadata) => op.isBucketed === true, + }, + }) + ).toEqual({ + dropTypes: ['replace_incompatible', 'replace_duplicate_incompatible'], + nextLabel: 'Minimum', + }); + }); + + it('returns combine_compatible drop type if the dragged column is compatible and the target one support multiple fields', () => { + const props = getDefaultProps(); + props.state.layers.first.columns = { + col1: mockedColumns.terms, + col2: { + ...mockedColumns.terms, + sourceField: 'bytes', + }, + }; + expect( + getDropProps({ + ...props, + target: { + ...props.target, + columnId: 'col2', + }, + source: { + ...mockedDndOperations.metric, + groupId: 'c', + }, + }) + ).toEqual({ + dropTypes: ['replace_compatible', 'replace_duplicate_compatible', 'combine_compatible'], + }); + }); + + it('returns no combine_compatible drop type if the target column uses rarity ordering', () => { + const props = getDefaultProps(); + props.state.layers.first.columns = { + col1: mockedColumns.terms, + col2: { + ...mockedColumns.terms, + sourceField: 'bytes', + params: { + ...(props.state.layers.first.columns.col1 as TermsIndexPatternColumn).params, + orderBy: { type: 'rare' }, + }, + } as TermsIndexPatternColumn, + }; + + expect( + getDropProps({ + ...props, + target: { + ...props.target, + groupId: 'a', + columnId: 'col2', + }, + source: { + ...mockedDndOperations.metric, + groupId: 'c', + }, + }) + ).toEqual({ + dropTypes: ['replace_compatible', 'replace_duplicate_compatible'], + }); + }); + + it('returns no combine drop type if the dragged column is compatible, the target one supports multiple fields but there are too many fields', () => { + const props = getDefaultProps(); + props.state.layers.first.columns.col2 = { + ...props.state.layers.first.columns.col1, + sourceField: 'source', + params: { + ...(props.state.layers.first.columns.col1 as TermsIndexPatternColumn).params, + secondaryFields: ['memory', 'bytes', 'geo.src'], // too many fields here + }, + } as TermsIndexPatternColumn; + + expect( + getDropProps({ + ...props, + target: { + ...props.target, + groupId: 'a', + columnId: 'col2', + }, + source: { + ...mockedDndOperations.metric, + groupId: 'c', + }, + }) + ).toEqual({ + dropTypes: ['replace_compatible', 'replace_duplicate_compatible'], + }); + }); + + it('returns combine_incompatible drop target types if dropping column to existing incompatible column which supports multiple fields', () => { + const props = getDefaultProps(); + props.state.layers.first.columns = { + col1: mockedColumns.terms, + col2: mockedColumns.sum, + }; + + expect( + getDropProps({ + ...props, + target: { + ...props.target, + groupId: 'a', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + // drag the sum over the top values + source: { + ...mockedDndOperations.bucket, + groupId: 'c', + filterOperation: undefined, + }, + }) + ).toEqual({ + dropTypes: [ + 'replace_incompatible', + 'replace_duplicate_incompatible', + 'swap_incompatible', + 'combine_incompatible', + ], + nextLabel: 'Top values', + }); + }); + }); + + describe('getDropProps between layers', () => { + it('allows dropping to the same group', () => { + const props = getDefaultProps(); + expect( + getDropProps({ + ...props, + source: { + ...mockedDndOperations.metric, + columnId: 'col1', + layerId: 'first', + groupId: 'c', + }, + target: { + ...props.target, + columnId: 'newId', + groupId: 'c', + layerId: 'second', + }, + }) + ).toEqual({ + dropTypes: ['move_compatible', 'duplicate_compatible'], + }); + }); + it('allows dropping to compatible groups', () => { + const props = getDefaultProps(); + expect( + getDropProps({ + ...props, + source: { + ...mockedDndOperations.metric, + columnId: 'col1', + layerId: 'first', + groupId: 'a', + }, + target: { + ...props.target, + columnId: 'newId', + groupId: 'c', + layerId: 'second', + }, + }) + ).toEqual({ + dropTypes: ['move_compatible', 'duplicate_compatible'], + }); + }); + it('allows incompatible drop', () => { + const props = getDefaultProps(); + expect( + getDropProps({ + ...props, + source: { + ...mockedDndOperations.metric, + columnId: 'col1', + layerId: 'first', + groupId: 'c', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + target: { + ...props.target, + columnId: 'newId', + groupId: 'c', + layerId: 'second', + filterOperations: (op: OperationMetadata) => !op.isBucketed, + }, + })?.dropTypes + ).toEqual(['move_incompatible', 'duplicate_incompatible']); + }); + it('allows dropping references', () => { + const props = getDefaultProps(); + const referenceDragging = { + columnId: 'col1', + groupId: 'a', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, + }; + + (generateId as jest.Mock).mockReturnValue(`ref1Copy`); + props.state = { + ...props.state, + layers: { + ...props.state.layers, + first: { + indexPatternId: 'first', + columnOrder: ['col1', 'ref1'], + columns: { + col1: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + operationType: 'cumulative_sum', + references: ['ref1'], + }, + ref1: { + label: 'Count of records', + dataType: 'number', + isBucketed: false, + sourceField: '___records___', + operationType: 'count', + }, + }, + }, + }, + }; + + expect( + getDropProps({ + ...props, + source: referenceDragging, + target: { + ...props.target, + columnId: 'newColumnId', + groupId: 'c', + layerId: 'second', + filterOperations: (op: OperationMetadata) => !op.isBucketed, + }, + })?.dropTypes + ).toEqual(['move_compatible', 'duplicate_compatible']); + }); + it('doesnt allow dropping for different index patterns', () => { + const props = getDefaultProps(); + props.state.layers.second.indexPatternId = 'different index'; + expect( + getDropProps({ + ...props, + source: { + ...mockedDndOperations.metric, + columnId: 'col1', + layerId: 'first', + groupId: 'c', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + target: { + ...props.target, + columnId: 'newId', + groupId: 'c', + layerId: 'second', + filterOperations: (op: OperationMetadata) => !op.isBucketed, + }, + })?.dropTypes + ).toEqual(undefined); + }); + + it('does not allow static value to be moved when not allowed', () => { + const props = getDefaultProps(); + props.state.layers = { + first: { + indexPatternId: 'first', + columns: { + col1: mockedColumns.dateHistogram, + colMetric: mockedColumns.count, + }, + columnOrder: ['col1', 'colMetric'], + incompleteColumns: {}, + }, + second: { + indexPatternId: 'first', + columns: { + staticValue: mockedColumns.staticValue, + }, + columnOrder: ['staticValue'], + incompleteColumns: {}, + }, + }; + expect( + getDropProps({ + ...props, + source: { + columnId: 'staticValue', + groupId: 'yReferenceLineLeft', + layerId: 'second', + id: 'staticValue', + humanData: { label: 'Label' }, + }, + target: { + layerId: 'first', + columnId: 'col1', + groupId: 'x', + } as DragDropOperation, + })?.dropTypes + ).toEqual(undefined); + }); + it('allow multiple drop types from terms to terms', () => { + const props = getDefaultProps(); + props.state.layers = { + first: { + indexPatternId: 'first', + columns: { + terms: mockedColumns.terms, + metric: mockedColumns.count, + }, + columnOrder: ['terms', 'metric'], + incompleteColumns: {}, + }, + second: { + indexPatternId: 'first', + columns: { + terms2: mockedColumns.terms2, + metric2: mockedColumns.count, + }, + columnOrder: ['terms2', 'metric2'], + incompleteColumns: {}, + }, + }; + expect( + getDropProps({ + ...props, + source: { + columnId: 'terms', + groupId: 'x', + layerId: 'first', + id: 'terms', + humanData: { label: 'Label' }, + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + target: { + columnId: 'terms2', + groupId: 'x', + layerId: 'second', + filterOperations: (op: OperationMetadata) => op.isBucketed, + } as DragDropOperation, + })?.dropTypes + ).toEqual([ + 'replace_compatible', + 'replace_duplicate_compatible', + 'swap_compatible', + 'combine_compatible', + ]); + }); + it('allow multiple drop types from metric on field to terms', () => { + const props = getDefaultProps(); + props.state.layers = { + first: { + indexPatternId: 'first', + columns: { + sum: mockedColumns.sum, + metric: mockedColumns.count, + }, + columnOrder: ['sum', 'metric'], + incompleteColumns: {}, + }, + second: { + indexPatternId: 'first', + columns: { + terms2: mockedColumns.terms2, + metric2: mockedColumns.count, + }, + columnOrder: ['terms2', 'metric2'], + incompleteColumns: {}, + }, + }; + expect( + getDropProps({ + ...props, + source: { + columnId: 'sum', + groupId: 'x', + layerId: 'first', + id: 'sum', + humanData: { label: 'Label' }, + filterOperations: (op: OperationMetadata) => !op.isBucketed, + }, + target: { + columnId: 'terms2', + groupId: 'x', + layerId: 'second', + filterOperations: (op: OperationMetadata) => op.isBucketed, + } as DragDropOperation, + })?.dropTypes + ).toEqual([ + 'replace_incompatible', + 'replace_duplicate_incompatible', + 'swap_incompatible', + 'combine_incompatible', + ]); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts index 3318b8c30909e..744033a2428fa 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts @@ -5,13 +5,7 @@ * 2.0. */ -import { - DatasourceDimensionDropProps, - isDraggedOperation, - DraggedOperation, - DropType, - VisualizationDimensionGroupConfig, -} from '../../../types'; +import { isOperation, DropType, DragDropOperation } from '../../../types'; import { getCurrentFieldsForOperation, getOperationDisplay, @@ -27,12 +21,18 @@ import { IndexPattern, IndexPatternField, DraggedField, + DataViewDragDropOperation, } from '../../types'; - -type GetDropProps = DatasourceDimensionDropProps & { - dragging?: DragContextState['dragging']; - groupId: string; -}; +import { + getDropPropsForSameGroup, + isOperationFromTheSameGroup, +} from '../../../editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils'; + +interface GetDropPropsArgs { + state: IndexPatternPrivateState; + source?: DragContextState['dragging']; + target: DragDropOperation; +} type DropProps = { dropTypes: DropType[]; nextLabel?: string } | undefined; @@ -41,7 +41,7 @@ const operationLabels = getOperationDisplay(); export function getNewOperation( field: IndexPatternField | undefined | false, filterOperations: (meta: OperationMetadata) => boolean, - targetColumn: GenericIndexPatternColumn, + targetColumn?: GenericIndexPatternColumn, prioritizedOperation?: GenericIndexPatternColumn['operationType'] ) { if (!field) { @@ -61,52 +61,50 @@ export function getNewOperation( return existsPrioritizedOperation ? prioritizedOperation : newOperations[0]; } -export function getField( - column: GenericIndexPatternColumn | undefined, - indexPattern: IndexPattern -) { +export function getField(column: GenericIndexPatternColumn | undefined, dataView: IndexPattern) { if (!column) { return; } - const field = (hasField(column) && indexPattern.getFieldByName(column.sourceField)) || undefined; + const field = (hasField(column) && dataView.getFieldByName(column.sourceField)) || undefined; return field; } -export function getDropProps(props: GetDropProps) { - const { state, columnId, layerId, dragging, groupId, filterOperations } = props; - if (!dragging) { +export function getDropProps(props: GetDropPropsArgs) { + const { state, source, target } = props; + if (!source) { return; } + const targetProps: DataViewDragDropOperation = { + ...target, + column: state.layers[target.layerId].columns[target.columnId], + dataView: state.indexPatterns[state.layers[target.layerId].indexPatternId], + }; - if (isDraggedField(dragging)) { - return getDropPropsForField({ ...props, dragging }); + if (isDraggedField(source)) { + return getDropPropsForField({ ...props, source, target: targetProps }); } - if ( - isDraggedOperation(dragging) && - dragging.layerId === layerId && - columnId !== dragging.columnId - ) { - const sourceColumn = state.layers[dragging.layerId].columns[dragging.columnId]; - const targetColumn = state.layers[layerId].columns[columnId]; - const isSameGroup = groupId === dragging.groupId; - if (isSameGroup) { - return getDropPropsForSameGroup(!targetColumn); - } - const layerIndexPattern = state.indexPatterns[state.layers[layerId].indexPatternId]; - - if (filterOperations(sourceColumn)) { - return getDropPropsForCompatibleGroup( - props.dimensionGroups, - dragging.columnId, - sourceColumn, - targetColumn, - layerIndexPattern - ); - } else if (hasTheSameField(sourceColumn, targetColumn)) { + if (isOperation(source)) { + const sourceProps: DataViewDragDropOperation = { + ...source, + column: state.layers[source.layerId]?.columns[source.columnId], + dataView: state.indexPatterns[state.layers[source.layerId]?.indexPatternId], + }; + if (!sourceProps.column) { return; - } else { - return getDropPropsFromIncompatibleGroup({ ...props, dragging }); + } + if (target.columnId !== source.columnId && targetProps.dataView === sourceProps.dataView) { + if (isOperationFromTheSameGroup(source, target)) { + return getDropPropsForSameGroup(!targetProps.column); + } + + if (targetProps.filterOperations?.(sourceProps?.column)) { + return getDropPropsForCompatibleGroup(sourceProps, targetProps); + } else if (hasTheSameField(sourceProps.column, targetProps.column)) { + return; + } else { + return getDropPropsFromIncompatibleGroup(sourceProps, targetProps); + } } } } @@ -126,14 +124,13 @@ function hasTheSameField( function getDropPropsForField({ state, - columnId, - layerId, - dragging, - filterOperations, -}: GetDropProps & { dragging: DraggedField }): DropProps { - const targetColumn = state.layers[layerId].columns[columnId]; - const isTheSameIndexPattern = state.layers[layerId].indexPatternId === dragging.indexPatternId; - const newOperation = getNewOperation(dragging.field, filterOperations, targetColumn); + source, + target, +}: GetDropPropsArgs & { source: DraggedField }): DropProps { + const targetColumn = state.layers[target.layerId].columns[target.columnId]; + const isTheSameIndexPattern = + state.layers[target.layerId].indexPatternId === source.indexPatternId; + const newOperation = getNewOperation(source.field, target.filterOperations, targetColumn); if (isTheSameIndexPattern && newOperation) { const nextLabel = operationLabels[newOperation].displayName; @@ -141,18 +138,13 @@ function getDropPropsForField({ if (!targetColumn) { return { dropTypes: ['field_add'], nextLabel }; } else if ( - (hasField(targetColumn) && targetColumn.sourceField !== dragging.field.name) || + (hasField(targetColumn) && targetColumn.sourceField !== source.field.name) || !hasField(targetColumn) ) { - const layerIndexPattern = state.indexPatterns[state.layers[layerId].indexPatternId]; + const layerDataView = state.indexPatterns[state.layers[target.layerId].indexPatternId]; return hasField(targetColumn) && - layerIndexPattern && - hasOperationSupportForMultipleFields( - layerIndexPattern, - targetColumn, - undefined, - dragging.field - ) + layerDataView && + hasOperationSupportForMultipleFields(layerDataView, targetColumn, undefined, source.field) ? { dropTypes: ['field_replace', 'field_combine'], } @@ -165,82 +157,68 @@ function getDropPropsForField({ return; } -function getDropPropsForSameGroup(isNew?: boolean): DropProps { - return !isNew ? { dropTypes: ['reorder'] } : { dropTypes: ['duplicate_compatible'] }; -} - function getDropPropsForCompatibleGroup( - dimensionGroups: VisualizationDimensionGroupConfig[], - sourceId: string, - sourceColumn?: GenericIndexPatternColumn, - targetColumn?: GenericIndexPatternColumn, - indexPattern?: IndexPattern + sourceProps: DataViewDragDropOperation, + targetProps: DataViewDragDropOperation ): DropProps { - const hasSameField = sourceColumn && hasTheSameField(sourceColumn, targetColumn); - - const canSwap = - targetColumn && - !hasSameField && - dimensionGroups - .find((group) => group.accessors.some((accessor) => accessor.columnId === sourceId)) - ?.filterOperations(targetColumn); - + if (!targetProps.column) { + return { dropTypes: ['move_compatible', 'duplicate_compatible'] }; + } + const canSwap = sourceProps.filterOperations?.(targetProps.column); const swapType: DropType[] = canSwap ? ['swap_compatible'] : []; - if (!targetColumn) { - return { dropTypes: ['move_compatible', 'duplicate_compatible', ...swapType] }; + const dropTypes: DropType[] = ['replace_compatible', 'replace_duplicate_compatible', ...swapType]; + if (!targetProps.dataView || !hasField(targetProps.column)) { + return { dropTypes }; } - if (!indexPattern || !hasField(targetColumn)) { - return { dropTypes: ['replace_compatible', 'replace_duplicate_compatible', ...swapType] }; - } - // With multi fields operations there are more combination of drops now - const dropTypes: DropType[] = []; - if (!hasSameField) { - dropTypes.push('replace_compatible', 'replace_duplicate_compatible'); - } - if (canSwap) { - dropTypes.push('swap_compatible'); - } - if (hasOperationSupportForMultipleFields(indexPattern, targetColumn, sourceColumn)) { + + if ( + hasOperationSupportForMultipleFields( + targetProps.dataView, + targetProps.column, + sourceProps.column + ) + ) { dropTypes.push('combine_compatible'); } - // return undefined if no drop action is available - if (!dropTypes.length) { - return; - } return { dropTypes, }; } -function getDropPropsFromIncompatibleGroup({ - state, - columnId, - layerId, - dragging, - filterOperations, -}: GetDropProps & { dragging: DraggedOperation }): DropProps { - const targetColumn = state.layers[layerId].columns[columnId]; - const sourceColumn = state.layers[dragging.layerId].columns[dragging.columnId]; - - const layerIndexPattern = state.indexPatterns[state.layers[layerId].indexPatternId]; - if (!layerIndexPattern) { +function getDropPropsFromIncompatibleGroup( + sourceProps: DataViewDragDropOperation, + targetProps: DataViewDragDropOperation +): DropProps { + if (!targetProps.dataView || !sourceProps.column) { return; } - const sourceField = getField(sourceColumn, layerIndexPattern); - const newOperationForSource = getNewOperation(sourceField, filterOperations, targetColumn); + const sourceField = getField(sourceProps.column, sourceProps.dataView); + const newOperationForSource = getNewOperation( + sourceField, + targetProps.filterOperations, + targetProps.column + ); if (newOperationForSource) { - const targetField = getField(targetColumn, layerIndexPattern); - const canSwap = Boolean(getNewOperation(targetField, dragging.filterOperations, sourceColumn)); + const targetField = getField(targetProps.column, targetProps.dataView); + const canSwap = Boolean( + getNewOperation(targetField, sourceProps.filterOperations, sourceProps.column) + ); const dropTypes: DropType[] = []; - if (targetColumn) { + if (targetProps.column) { dropTypes.push('replace_incompatible', 'replace_duplicate_incompatible'); if (canSwap) { dropTypes.push('swap_incompatible'); } - if (hasOperationSupportForMultipleFields(layerIndexPattern, targetColumn, sourceColumn)) { + if ( + hasOperationSupportForMultipleFields( + targetProps.dataView, + targetProps.column, + sourceProps.column + ) + ) { dropTypes.push('combine_incompatible'); } } else { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/mocks.ts new file mode 100644 index 0000000000000..40121cf99f546 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/mocks.ts @@ -0,0 +1,292 @@ +/* + * 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 { IndexPattern, IndexPatternLayer } from '../../types'; +import { documentField } from '../../document_field'; +import { OperationMetadata } from '../../../types'; +import { + DateHistogramIndexPatternColumn, + GenericIndexPatternColumn, + StaticValueIndexPatternColumn, + TermsIndexPatternColumn, +} from '../../operations'; +import { getFieldByNameFactory } from '../../pure_helpers'; +jest.mock('../../../id_generator'); + +export const mockDataViews = (): Record => { + const fields = [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'memory', + displayName: 'memory', + type: 'number', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'source', + displayName: 'source', + type: 'string', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'src', + displayName: 'src', + type: 'string', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'dest', + displayName: 'dest', + type: 'string', + aggregatable: true, + searchable: true, + exists: true, + }, + documentField, + ]; + return { + first: { + id: 'first', + title: 'first', + timeFieldName: 'timestamp', + hasRestrictions: false, + fields, + getFieldByName: getFieldByNameFactory(fields), + }, + second: { + id: 'second', + title: 'my-fake-restricted-pattern', + hasRestrictions: true, + timeFieldName: 'timestamp', + fields: [fields[0]], + getFieldByName: getFieldByNameFactory([fields[0]]), + }, + }; +}; + +export const mockedColumns: Record = { + count: { + label: 'Count of records', + dataType: 'number', + isBucketed: false, + sourceField: '___records___', + operationType: 'count', + }, + staticValue: { + label: 'Static value: 0.75', + dataType: 'number', + operationType: 'static_value', + isStaticValue: true, + isBucketed: false, + scale: 'ratio', + params: { + value: '0.75', + }, + references: [], + } as StaticValueIndexPatternColumn, + dateHistogram: { + label: 'Date histogram of timestamp', + customLabel: true, + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: '1d', + }, + sourceField: 'timestamp', + } as DateHistogramIndexPatternColumn, + dateHistogramCopy: { + label: 'Date histogram of timestamp (1)', + customLabel: true, + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: '1d', + }, + sourceField: 'timestamp', + } as DateHistogramIndexPatternColumn, + terms: { + label: 'Top 10 values of src', + dataType: 'string', + isBucketed: true, + // Private + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'desc', + size: 10, + }, + sourceField: 'src', + } as TermsIndexPatternColumn, + terms2: { + label: 'Top 10 values of dest', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'desc', + size: 10, + }, + sourceField: 'dest', + } as TermsIndexPatternColumn, + sum: { + label: 'Sum of bytes', + dataType: 'number', + isBucketed: false, + operationType: 'sum', + sourceField: 'bytes', + } as GenericIndexPatternColumn, + median: { + label: 'Median of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'median', + sourceField: 'bytes', + } as GenericIndexPatternColumn, + uniqueCount: { + label: 'Unique count of bytes', + dataType: 'number', + isBucketed: false, + sourceField: 'bytes', + operationType: 'unique_count', + } as GenericIndexPatternColumn, +}; + +export const mockedLayers: Record IndexPatternLayer> = { + singleColumnLayer: (id = 'col1') => ({ + indexPatternId: 'first', + columnOrder: [id], + columns: { + [id]: mockedColumns.dateHistogram, + }, + incompleteColumns: {}, + }), + doubleColumnLayer: (id1 = 'col1', id2 = 'col2') => ({ + indexPatternId: 'first', + columnOrder: [id1, id2], + columns: { + [id1]: mockedColumns.dateHistogram, + [id2]: mockedColumns.terms, + }, + incompleteColumns: {}, + }), + multipleColumnsLayer: (id1 = 'col1', id2 = 'col2', id3 = 'col3', id4 = 'col4') => ({ + indexPatternId: 'first', + columnOrder: [id1, id2, id3, id4], + columns: { + [id1]: mockedColumns.dateHistogram, + [id2]: mockedColumns.terms, + [id3]: mockedColumns.terms2, + [id4]: mockedColumns.median, + }, + }), + emptyLayer: () => ({ + indexPatternId: 'first', + columnOrder: [], + columns: {}, + }), +}; + +export const mockedDraggedField = { + field: { type: 'number', name: 'bytes', aggregatable: true }, + indexPatternId: 'first', + id: 'bar', + humanData: { label: 'Label' }, +}; + +export const mockedDndOperations = { + notFiltering: { + layerId: 'first', + groupId: 'a', + filterOperations: () => true, + columnId: 'col1', + id: 'col1', + humanData: { label: 'Column 1' }, + }, + metric: { + layerId: 'first', + groupId: 'a', + columnId: 'col1', + filterOperations: (op: OperationMetadata) => !op.isBucketed, + id: 'col1', + humanData: { label: 'Column 1' }, + }, + numericalOnly: { + layerId: 'first', + groupId: 'a', + columnId: 'col1', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + id: 'col1', + humanData: { label: 'Column 1' }, + }, + bucket: { + columnId: 'col2', + groupId: 'b', + layerId: 'first', + id: 'col2', + humanData: { label: 'Column 2' }, + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + staticValue: { + columnId: 'col1', + groupId: 'b', + layerId: 'first', + id: 'col1', + humanData: { label: 'Column 2' }, + filterOperations: (op: OperationMetadata) => !!op.isStaticValue, + }, + bucket2: { + columnId: 'col3', + groupId: 'b', + layerId: 'first', + id: 'col3', + humanData: { + label: '', + }, + }, + metricC: { + columnId: 'col4', + groupId: 'c', + layerId: 'first', + id: 'col4', + humanData: { + label: '', + }, + filterOperations: (op: OperationMetadata) => !op.isBucketed, + }, +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.test.ts new file mode 100644 index 0000000000000..12acf46c58380 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.test.ts @@ -0,0 +1,2259 @@ +/* + * 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 { onDrop } from './on_drop_handler'; +import { IndexPatternPrivateState } from '../../types'; +import { OperationMetadata, DropType, DatasourceDimensionDropHandlerProps } from '../../../types'; +import { FormulaIndexPatternColumn, MedianIndexPatternColumn } from '../../operations'; +import { generateId } from '../../../id_generator'; +import { + mockDataViews, + mockedLayers, + mockedDraggedField, + mockedDndOperations, + mockedColumns, +} from './mocks'; + +jest.mock('../../../id_generator'); + +const dimensionGroups = [ + { + accessors: [], + groupId: 'a', + supportsMoreColumns: true, + hideGrouping: true, + groupLabel: '', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + { + accessors: [{ columnId: 'col1' }, { columnId: 'col2' }, { columnId: 'col3' }], + groupId: 'b', + supportsMoreColumns: true, + hideGrouping: true, + groupLabel: '', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + { + accessors: [{ columnId: 'col4' }], + groupId: 'c', + supportsMoreColumns: true, + hideGrouping: true, + groupLabel: '', + filterOperations: (op: OperationMetadata) => op.isBucketed === false, + }, +]; + +function getStateWithMultiFieldColumn(state: IndexPatternPrivateState) { + return { + ...state, + layers: { + ...state.layers, + first: { + ...state.layers.first, + columns: { + col1: mockedColumns.terms2, + }, + }, + }, + }; +} + +describe('IndexPatternDimensionEditorPanel: onDrop', () => { + let state: IndexPatternPrivateState; + let setState: jest.Mock; + let defaultProps: DatasourceDimensionDropHandlerProps; + + beforeEach(() => { + state = { + indexPatternRefs: [], + indexPatterns: mockDataViews(), + currentIndexPatternId: 'first', + isFirstExistenceFetch: false, + existingFields: { + first: { + timestamp: true, + bytes: true, + memory: true, + source: true, + }, + }, + layers: { + first: mockedLayers.singleColumnLayer(), + second: mockedLayers.emptyLayer(), + }, + }; + + setState = jest.fn(); + + defaultProps = { + dropType: 'reorder', + source: { name: 'bar', id: 'bar', humanData: { label: 'Label' } }, + target: mockedDndOperations.notFiltering, + state, + setState, + dimensionGroups: [], + }; + + jest.clearAllMocks(); + }); + + describe('dropping a field', () => { + it('updates a column when a field is dropped', () => { + onDrop({ + ...defaultProps, + source: mockedDraggedField, + dropType: 'field_replace', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + ...state.layers, + first: expect.objectContaining({ + columns: expect.objectContaining({ + col1: expect.objectContaining({ + dataType: 'number', + sourceField: mockedDraggedField.field.name, + }), + }), + }), + }, + }); + }); + it('selects the specific operation that was valid on drop', () => { + onDrop({ + ...defaultProps, + source: mockedDraggedField, + dropType: 'field_replace', + target: { + ...defaultProps.target, + filterOperations: (op: OperationMetadata) => op.isBucketed, + columnId: 'col2', + }, + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + ...state.layers, + first: { + ...state.layers.first, + columnOrder: ['col1', 'col2'], + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + dataType: 'number', + sourceField: mockedDraggedField.field.name, + }), + }, + }, + }, + }); + }); + it('keeps the operation when dropping a different compatible field', () => { + onDrop({ + ...defaultProps, + source: { + humanData: { label: 'Label1' }, + field: { name: 'memory', type: 'number', aggregatable: true }, + indexPatternId: 'first', + id: '1', + }, + state: { + ...state, + layers: { + ...state.layers, + first: { + indexPatternId: 'first', + columnOrder: ['col1'], + columns: { + col1: mockedColumns.sum, + }, + }, + }, + }, + dropType: 'field_replace', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + ...state.layers, + first: expect.objectContaining({ + columns: expect.objectContaining({ + col1: expect.objectContaining({ + operationType: 'sum', + dataType: 'number', + sourceField: 'memory', + }), + }), + }), + }, + }); + }); + it('appends the dropped column when a field is dropped', () => { + onDrop({ + ...defaultProps, + source: mockedDraggedField, + dropType: 'field_replace', + target: { + ...defaultProps.target, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + }, + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + ...state.layers, + first: { + ...state.layers.first, + columnOrder: ['col1', 'col2'], + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + dataType: 'number', + sourceField: mockedDraggedField.field.name, + }), + }, + }, + }, + }); + }); + it('dimensionGroups are defined - appends the dropped column in the right place when a field is dropped', () => { + const testState = { ...state }; + testState.layers.first = { ...mockedLayers.multipleColumnsLayer() }; + // config: + // a: + // b: col1, col2, col3 + // c: col4 + // dragging field into newCol in group a + + onDrop({ + ...defaultProps, + source: mockedDraggedField, + dimensionGroups, + dropType: 'field_add', + target: { + ...defaultProps.target, + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + groupId: 'a', + columnId: 'newCol', + }, + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], + columns: { + newCol: expect.objectContaining({ + dataType: 'number', + sourceField: mockedDraggedField.field.name, + }), + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + incompleteColumns: {}, + }, + }, + }); + }); + + it('appends the new field to the column that supports multiple fields when a field is dropped', () => { + state = getStateWithMultiFieldColumn(state); + onDrop({ + ...defaultProps, + state, + source: mockedDraggedField, + dropType: 'field_combine', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + ...state.layers, + first: expect.objectContaining({ + columns: expect.objectContaining({ + col1: expect.objectContaining({ + dataType: 'string', + sourceField: 'dest', + params: expect.objectContaining({ + secondaryFields: [mockedDraggedField.field.name], + }), + }), + }), + }), + }, + }); + }); + }); + + describe('dropping a dimension', () => { + it('sets correct order in group for metric and bucket columns when duplicating a column in group', () => { + const testState: IndexPatternPrivateState = { + ...state, + layers: { + ...state.layers, + first: { + indexPatternId: 'first', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: mockedColumns.dateHistogram, + col2: mockedColumns.terms, + col3: mockedColumns.sum, + }, + }, + }, + }; + + const referenceDragging = { + columnId: 'col3', + groupId: 'a', + layerId: 'first', + id: 'col3', + humanData: { label: 'Label' }, + }; + + onDrop({ + ...defaultProps, + source: referenceDragging, + state: testState, + dropType: 'duplicate_compatible', + target: { + ...defaultProps.target, + columnId: 'newCol', + }, + }); + // metric is appended + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'col3', 'newCol'], + columns: { + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + newCol: testState.layers.first.columns.col3, + }, + }, + }, + }); + + const bucketDragging = { + columnId: 'col2', + groupId: 'a', + layerId: 'first', + id: 'col2', + humanData: { label: 'Label' }, + }; + + onDrop({ + ...defaultProps, + state: testState, + dropType: 'duplicate_compatible', + source: bucketDragging, + target: { + ...defaultProps.target, + columnId: 'newCol', + }, + }); + + // bucket is placed after the last existing bucket + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'newCol', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + newCol: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + }, + }, + }, + }); + }); + + it('when duplicating fullReference column, the referenced columns get duplicated too', () => { + (generateId as jest.Mock).mockReturnValue(`ref1Copy`); + const testState: IndexPatternPrivateState = { + ...state, + layers: { + ...state.layers, + first: { + indexPatternId: '1', + columnOrder: ['col1', 'ref1'], + columns: { + col1: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + operationType: 'cumulative_sum', + references: ['ref1'], + }, + ref1: mockedColumns.count, + }, + }, + }, + }; + const referenceDragging = { + columnId: 'col1', + groupId: 'a', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, + }; + onDrop({ + ...defaultProps, + source: referenceDragging, + state: testState, + dropType: 'duplicate_compatible', + target: { + ...defaultProps.target, + columnId: 'col1Copy', + }, + }); + + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'ref1', 'ref1Copy', 'col1Copy'], + columns: { + ref1: testState.layers.first.columns.ref1, + col1: testState.layers.first.columns.col1, + ref1Copy: { ...testState.layers.first.columns.ref1 }, + col1Copy: { + ...testState.layers.first.columns.col1, + references: ['ref1Copy'], + }, + }, + }, + }, + }); + }); + + it('when duplicating fullReference column, the multiple referenced columns get duplicated too', () => { + (generateId as jest.Mock).mockReturnValueOnce(`ref1Copy`); + (generateId as jest.Mock).mockReturnValueOnce(`ref2Copy`); + const testState: IndexPatternPrivateState = { + ...state, + layers: { + ...state.layers, + first: { + indexPatternId: '1', + columnOrder: ['col1', 'ref1'], + columns: { + col1: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + operationType: 'cumulative_sum', + references: ['ref1', 'ref2'], + }, + ref1: mockedColumns.count, + ref2: mockedColumns.uniqueCount, + }, + }, + }, + }; + const metricDragging = { + columnId: 'col1', + groupId: 'a', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, + }; + onDrop({ + ...defaultProps, + source: metricDragging, + state: testState, + dropType: 'duplicate_compatible', + target: { + ...defaultProps.target, + columnId: 'col1Copy', + }, + }); + + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'ref1', 'ref2', 'ref1Copy', 'ref2Copy', 'col1Copy'], + columns: { + ref1: testState.layers.first.columns.ref1, + ref2: testState.layers.first.columns.ref2, + col1: testState.layers.first.columns.col1, + ref2Copy: { ...testState.layers.first.columns.ref2 }, + ref1Copy: { ...testState.layers.first.columns.ref1 }, + col1Copy: { + ...testState.layers.first.columns.col1, + references: ['ref1Copy', 'ref2Copy'], + }, + }, + }, + }, + }); + }); + + it('when duplicating fullReference column, the referenced columns get duplicated', () => { + (generateId as jest.Mock).mockReturnValueOnce(`ref1Copy`); + (generateId as jest.Mock).mockReturnValueOnce(`ref2Copy`); + const testState: IndexPatternPrivateState = { + ...state, + layers: { + ...state.layers, + first: { + indexPatternId: '1', + columnOrder: ['ref2', 'ref1', 'col1'], + columns: { + col1: { + label: 'Test reference', + dataType: 'number', + isBucketed: false, + operationType: 'cumulative_sum', + references: ['ref1', 'ref2'], + }, + ref1: mockedColumns.count, + ref2: { + label: 'Unique count of bytes', + dataType: 'number', + isBucketed: false, + sourceField: 'bytes', + operationType: 'unique_count', + }, + }, + }, + }, + }; + const refDragging = { + columnId: 'col1', + groupId: 'a', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, + }; + onDrop({ + ...defaultProps, + source: refDragging, + state: testState, + dropType: 'duplicate_compatible', + target: { + ...defaultProps.target, + columnId: 'col1Copy', + }, + }); + + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['ref2', 'ref1', 'col1', 'ref1Copy', 'ref2Copy', 'col1Copy'], + columns: { + ref1: testState.layers.first.columns.ref1, + ref2: testState.layers.first.columns.ref2, + col1: testState.layers.first.columns.col1, + ref2Copy: { ...testState.layers.first.columns.ref2 }, + ref1Copy: { + ...testState.layers.first.columns.ref1, + }, + col1Copy: { + ...testState.layers.first.columns.col1, + references: ['ref1Copy', 'ref2Copy'], + }, + }, + }, + }, + }); + }); + + it('sets correct order in group when reordering a column in group', () => { + const testState = { + ...state, + layers: { + ...state.layers, + first: { + indexPatternId: 'first', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: mockedColumns.dateHistogram, + col2: mockedColumns.terms, + col3: mockedColumns.terms2, + }, + }, + }, + }; + + const defaultReorderDropParams = { + ...defaultProps, + target: { + ...defaultProps.target, + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + }, + source: mockedDndOperations.metric, + state: testState, + dropType: 'reorder' as DropType, + }; + + const stateWithColumnOrder = (columnOrder: string[]) => { + return { + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder, + columns: { + ...testState.layers.first.columns, + }, + }, + }, + }; + }; + + // first element to last + onDrop({ + ...defaultReorderDropParams, + target: { + ...defaultReorderDropParams.target, + columnId: 'col3', + }, + }); + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col2', 'col3', 'col1'])); + + // last element to first + onDrop({ + ...defaultReorderDropParams, + target: { + ...defaultReorderDropParams.target, + columnId: 'col1', + }, + source: { + humanData: { label: 'Label1' }, + columnId: 'col3', + groupId: 'a', + layerId: 'first', + id: 'col3', + }, + }); + expect(setState).toBeCalledTimes(2); + expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col3', 'col1', 'col2'])); + + // middle column to first + onDrop({ + ...defaultReorderDropParams, + target: { + ...defaultReorderDropParams.target, + columnId: 'col1', + }, + source: { + humanData: { label: 'Label1' }, + columnId: 'col2', + groupId: 'a', + layerId: 'first', + id: 'col2', + }, + }); + expect(setState).toBeCalledTimes(3); + expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col2', 'col1', 'col3'])); + + // middle column to last + onDrop({ + ...defaultReorderDropParams, + target: { + ...defaultReorderDropParams.target, + columnId: 'col3', + }, + source: { + humanData: { label: 'Label1' }, + columnId: 'col2', + groupId: 'a', + layerId: 'first', + id: 'col2', + }, + }); + expect(setState).toBeCalledTimes(4); + expect(setState).toHaveBeenCalledWith(stateWithColumnOrder(['col1', 'col3', 'col2'])); + }); + + it('updates the column id when moving an operation to an empty dimension', () => { + onDrop({ + ...defaultProps, + source: mockedDndOperations.metric, + target: { + ...defaultProps.target, + columnId: 'col2', + }, + dropType: 'move_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + ...state.layers, + first: { + ...state.layers.first, + columnOrder: ['col2'], + columns: { + col2: state.layers.first.columns.col1, + }, + }, + }, + }); + }); + + it('replaces an operation when moving to a populated dimension', () => { + const testState = { ...state }; + testState.layers.first = { + indexPatternId: 'first', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + col2: mockedColumns.terms, + col3: mockedColumns.count, + }, + }; + + onDrop({ + ...defaultProps, + source: mockedDndOperations.bucket, + state: testState, + dropType: 'replace_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + incompleteColumns: {}, + columnOrder: ['col1', 'col3'], + columns: { + col1: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + }, + }, + }, + }); + }); + + it('when combine compatible columns should append dropped column fields into the target one', () => { + state = getStateWithMultiFieldColumn(state); + state.layers.first.columns = { + ...state.layers.first.columns, + col2: mockedColumns.terms, + }; + onDrop({ + ...defaultProps, + state, + source: { + columnId: 'col2', + groupId: 'a', + layerId: 'first', + id: 'col2', + humanData: { label: 'Label' }, + }, + dropType: 'combine_compatible', + target: { + ...defaultProps.target, + columnId: 'col1', + groupId: 'a', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + ...state.layers, + first: expect.objectContaining({ + columns: expect.objectContaining({ + col1: expect.objectContaining({ + dataType: 'string', + sourceField: 'dest', + params: expect.objectContaining({ secondaryFields: ['src'] }), + }), + }), + }), + }, + }); + }); + + describe('dimension group aware ordering and copying', () => { + let testState: IndexPatternPrivateState; + beforeEach(() => { + testState = { ...state }; + testState.layers.first = { ...mockedLayers.multipleColumnsLayer() }; + }); + + it('respects groups on moving operations between compatible groups', () => { + // config: + // a: + // b: col1, col2, col3 + // c: col4 + // dragging col2 into newCol in group a + onDrop({ + ...defaultProps, + target: { + ...defaultProps.target, + columnId: 'newCol', + groupId: 'a', + }, + source: mockedDndOperations.bucket, + state: testState, + dimensionGroups, + dropType: 'move_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + incompleteColumns: {}, + columnOrder: ['newCol', 'col1', 'col3', 'col4'], + columns: { + newCol: testState.layers.first.columns.col2, + col1: testState.layers.first.columns.col1, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('respects groups on duplicating operations between compatible groups', () => { + // config: + // a: + // b: col1, col2, col3 + // c: col4 + // dragging col2 into newCol in group a + onDrop({ + ...defaultProps, + target: { + ...defaultProps.target, + columnId: 'newCol', + groupId: 'a', + }, + source: mockedDndOperations.bucket, + state: testState, + dimensionGroups, + dropType: 'duplicate_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], + columns: { + newCol: testState.layers.first.columns.col2, + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('respects groups on moving operations between compatible groups with overwrite', () => { + // config: + // a: col1, + // b: col2, col3 + // c: col4 + // dragging col3 onto col1 in group a + onDrop({ + ...defaultProps, + target: { + ...defaultProps.target, + columnId: 'col1', + groupId: 'a', + }, + source: mockedDndOperations.bucket2, + state: testState, + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + dropType: 'move_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + incompleteColumns: {}, + columnOrder: ['col1', 'col2', 'col4'], + columns: { + col1: testState.layers.first.columns.col3, + col2: testState.layers.first.columns.col2, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('respects groups on moving operations if some columns are not listed in groups', () => { + // config: + // a: col1, + // b: col2, col3 + // c: col4 + // col5, col6 not in visualization groups + // dragging col3 onto col1 in group a + onDrop({ + ...defaultProps, + source: mockedDndOperations.bucket2, + target: { + ...defaultProps.target, + columnId: 'col1', + groupId: 'a', + }, + state: { + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'col3', 'col4', 'col5', 'col6'], + columns: { + ...testState.layers.first.columns, + col5: { + dataType: 'number', + operationType: 'count', + label: '', + isBucketed: false, + sourceField: '___records___', + customLabel: true, + }, + col6: { + dataType: 'number', + operationType: 'count', + label: '', + isBucketed: false, + sourceField: '___records___', + customLabel: true, + }, + }, + }, + }, + }, + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + dropType: 'move_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + incompleteColumns: {}, + columnOrder: ['col1', 'col2', 'col4', 'col5', 'col6'], + columns: { + col1: testState.layers.first.columns.col3, + col2: testState.layers.first.columns.col2, + col4: testState.layers.first.columns.col4, + col5: expect.objectContaining({ + dataType: 'number', + operationType: 'count', + label: '', + isBucketed: false, + sourceField: '___records___', + }), + col6: expect.objectContaining({ + dataType: 'number', + operationType: 'count', + label: '', + isBucketed: false, + sourceField: '___records___', + }), + }, + }, + }, + }); + }); + + it('respects groups on duplicating operations between compatible groups with overwrite', () => { + // config: + // a: col1, + // b: col2, col3 + // c: col4 + // dragging col3 onto col1 in group a + + onDrop({ + ...defaultProps, + source: mockedDndOperations.bucket2, + state: testState, + target: { + ...defaultProps.target, + columnId: 'col1', + groupId: 'a', + }, + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + dropType: 'duplicate_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'col3', 'col4'], + columns: { + col1: testState.layers.first.columns.col3, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('moves newly created dimension to the bottom of the current group', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col1 into newCol in group b + onDrop({ + ...defaultProps, + dropType: 'move_compatible', + source: mockedDndOperations.metric, + state: testState, + target: { + ...defaultProps.target, + columnId: 'newCol', + groupId: 'b', + }, + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + incompleteColumns: {}, + columnOrder: ['col2', 'col3', 'newCol', 'col4'], + columns: { + newCol: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('copies column to the bottom of the current group', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // copying col1 within group a + onDrop({ + ...defaultProps, + dropType: 'duplicate_compatible', + target: { + ...defaultProps.target, + columnId: 'newCol', + groupId: 'a', + }, + source: mockedDndOperations.metric, + state: testState, + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'newCol', 'col2', 'col3', 'col4'], + columns: { + col1: testState.layers.first.columns.col1, + newCol: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('appends the dropped column in the right place respecting custom nestingOrder', () => { + // config: + // a: + // b: col1, col2, col3 + // c: col4 + // dragging field into newCol in group a + + onDrop({ + ...defaultProps, + source: mockedDraggedField, + target: { + ...defaultProps.target, + columnId: 'newCol', + groupId: 'a', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + }, + dimensionGroups: [ + // a and b are ordered in reverse visually, but nesting order keeps them in place for column order + { ...dimensionGroups[1], nestingOrder: 1 }, + { ...dimensionGroups[0], nestingOrder: 0 }, + { ...dimensionGroups[2] }, + ], + dropType: 'field_add', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + ...state.layers, + first: { + ...testState.layers.first, + columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], + columns: { + newCol: expect.objectContaining({ + dataType: 'number', + sourceField: mockedDraggedField.field.name, + }), + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + incompleteColumns: {}, + }, + }, + }); + }); + + it('moves incompatible column to the bottom of the target group', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col4 into newCol in group a + + onDrop({ + ...defaultProps, + dropType: 'move_incompatible', + source: mockedDndOperations.metricC, + state: testState, + target: { + ...defaultProps.target, + columnId: 'newCol', + groupId: 'a', + }, + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'newCol', 'col2', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + newCol: expect.objectContaining({ + sourceField: (testState.layers.first.columns.col4 as MedianIndexPatternColumn) + .sourceField, + }), + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + }, + incompleteColumns: {}, + }, + }, + }); + }); + + it('copies incompatible column to the bottom of the target group', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col4 into newCol in group a + + onDrop({ + ...defaultProps, + dropType: 'duplicate_incompatible', + source: mockedDndOperations.metricC, + state: testState, + target: { + ...defaultProps.target, + columnId: 'newCol', + groupId: 'a', + }, + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'newCol', 'col2', 'col3', 'col4'], + columns: { + col1: testState.layers.first.columns.col1, + newCol: expect.objectContaining({ + sourceField: (testState.layers.first.columns.col4 as MedianIndexPatternColumn) + .sourceField, + }), + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + incompleteColumns: {}, + }, + }, + }); + }); + + it('moves incompatible column with overwrite keeping order of target column', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col4 into col2 in group b + + onDrop({ + ...defaultProps, + dropType: 'move_incompatible', + source: mockedDndOperations.metricC, + state: testState, + target: { + ...defaultProps.target, + columnId: 'col2', + groupId: 'b', + }, + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + col2: { + isBucketed: true, + label: 'Top 10 values of bytes', + operationType: 'terms', + sourceField: 'bytes', + dataType: 'number', + params: { + orderBy: { + type: 'alphabetical', + }, + orderDirection: 'desc', + size: 10, + parentFormat: { id: 'terms' }, + }, + }, + col3: testState.layers.first.columns.col3, + }, + incompleteColumns: {}, + }, + }, + }); + }); + + it('when swapping compatibly, columns carry order', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col4 into col1 + + onDrop({ + ...defaultProps, + target: { + ...defaultProps.target, + columnId: 'col1', + groupId: 'a', + }, + source: mockedDndOperations.metricC, + dropType: 'swap_compatible', + state: testState, + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'col3', 'col4'], + columns: { + col1: testState.layers.first.columns.col4, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col1, + }, + }, + }, + }); + }); + + it('when swapping incompatibly, newly created columns take order from the columns they replace', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col4 into col2 + + onDrop({ + ...defaultProps, + target: { + ...defaultProps.target, + columnId: 'col2', + groupId: 'b', + }, + dropType: 'swap_incompatible', + source: mockedDndOperations.metricC, + state: testState, + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + ...testState.layers, + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'col3', 'col4'], + columns: { + col1: testState.layers.first.columns.col1, + col2: { + isBucketed: true, + label: 'Top 10 values of bytes', + operationType: 'terms', + sourceField: 'bytes', + dataType: 'number', + params: { + orderBy: { + type: 'alphabetical', + }, + orderDirection: 'desc', + parentFormat: { id: 'terms' }, + size: 10, + }, + }, + col3: testState.layers.first.columns.col3, + col4: { + isBucketed: false, + label: 'Unique count of src', + filter: undefined, + operationType: 'unique_count', + sourceField: 'src', + timeShift: undefined, + dataType: 'number', + params: { + emptyAsNull: true, + }, + scale: 'ratio', + }, + }, + incompleteColumns: {}, + }, + }, + }); + }); + }); + + describe('onDrop between layers', () => { + const defaultDimensionGroups = [ + { + groupId: 'x', + groupLabel: 'Horizontal axis', + accessors: [], + supportsMoreColumns: true, + dataTestSubj: 'lnsXY_xDimensionPanel', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + { + groupId: 'y', + groupLabel: 'Vertical axis', + accessors: [], + supportsMoreColumns: true, + required: true, + dataTestSubj: 'lnsXY_yDimensionPanel', + enableDimensionEditor: true, + filterOperations: (op: OperationMetadata) => !op.isBucketed, + }, + { + groupId: 'breakdown', + groupLabel: 'Break down by', + accessors: [], + supportsMoreColumns: true, + dataTestSubj: 'lnsXY_splitDimensionPanel', + required: false, + enableDimensionEditor: true, + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + ]; + describe('simple operations', () => { + let props: DatasourceDimensionDropHandlerProps; + beforeEach(() => { + setState = jest.fn(); + + props = { + state: { + indexPatternRefs: [], + indexPatterns: mockDataViews(), + currentIndexPatternId: 'first', + isFirstExistenceFetch: false, + existingFields: { + first: { + timestamp: true, + bytes: true, + memory: true, + source: true, + }, + }, + layers: { + first: mockedLayers.singleColumnLayer(), + second: mockedLayers.multipleColumnsLayer('col2', 'col3', 'col4', 'col5'), + }, + }, + setState: jest.fn(), + source: { + id: 'col1', + humanData: { label: '2' }, + columnId: 'col1', + groupId: 'x', + layerId: 'first', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + target: { + filterOperations: (op: OperationMetadata) => op.isBucketed, + columnId: 'newCol', + groupId: 'x', + layerId: 'second', + }, + dimensionGroups: defaultDimensionGroups, + dropType: 'move_compatible', + }; + jest.clearAllMocks(); + }); + it('doesnt allow dropping for different data views', () => { + props.state.layers.second.indexPatternId = 'second'; + expect(onDrop(props)).toEqual(false); + expect(props.setState).not.toHaveBeenCalled(); + }); + it('move_compatible; allows dropping to the compatible group in different layer to empty column', () => { + expect(onDrop(props)).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + first: { + ...mockedLayers.emptyLayer(), + incompleteColumns: {}, + }, + second: { + columnOrder: ['col2', 'col3', 'col4', 'newCol', 'col5'], + columns: { + ...props.state.layers.second.columns, + newCol: mockedColumns.dateHistogram, + }, + indexPatternId: 'first', + }, + }, + }); + }); + it('duplicate_compatible: allows dropping to the compatible group in different layer to empty column', () => { + expect(onDrop({ ...props, dropType: 'duplicate_compatible' })).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + second: { + columnOrder: ['col2', 'col3', 'col4', 'newCol', 'col5'], + columns: { + ...props.state.layers.second.columns, + newCol: mockedColumns.dateHistogram, + }, + indexPatternId: 'first', + }, + }, + }); + }); + it('swap_compatible: allows dropping to compatible group to replace an existing column', () => { + props = { + ...props, + + target: { + ...props.target, + columnId: 'col4', + groupId: 'breakdown', + layerId: 'second', + }, + dropType: 'swap_compatible', + }; + + expect(onDrop(props)).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + first: { + ...props.state.layers.first, + columns: { + col1: props.state.layers.second.columns.col4, + }, + }, + second: { + ...props.state.layers.second, + columns: { + ...props.state.layers.second.columns, + col4: props.state.layers.first.columns.col1, + }, + }, + }, + }); + }); + it('replace_compatible: allows dropping to compatible group to replace an existing column', () => { + props = { + ...props, + target: { + ...props.target, + columnId: 'col4', + groupId: 'breakdown', + layerId: 'second', + }, + dropType: 'replace_compatible', + }; + expect(onDrop(props)).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + first: { + ...mockedLayers.emptyLayer(), + incompleteColumns: {}, + }, + second: { + columnOrder: ['col2', 'col3', 'col4', 'col5'], + columns: { + ...props.state.layers.second.columns, + col4: mockedColumns.dateHistogram, + }, + indexPatternId: 'first', + }, + }, + }); + }); + it('replace_duplicate_compatible: allows dropping to compatible group to replace an existing column', () => { + props = { + ...props, + target: { + ...props.target, + columnId: 'col4', + groupId: 'breakdown', + layerId: 'second', + }, + dropType: 'replace_duplicate_compatible', + }; + + expect(onDrop(props)).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + second: { + columnOrder: ['col2', 'col3', 'col4', 'col5'], + columns: { + ...props.state.layers.second.columns, + col4: mockedColumns.dateHistogram, + }, + indexPatternId: 'first', + }, + }, + }); + }); + it('replace_duplicate_incompatible: allows dropping to compatible group to replace an existing column', () => { + props = { + ...props, + target: { + ...props.target, + columnId: 'col5', + groupId: 'y', + layerId: 'second', + filterOperations: (op) => !op.isBucketed, + }, + dropType: 'replace_duplicate_incompatible', + }; + + expect(onDrop(props)).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + second: { + columnOrder: ['col2', 'col3', 'col4', 'col5'], + columns: { + ...props.state.layers.second.columns, + col5: { + dataType: 'date', + isBucketed: false, + label: 'Minimum of timestampLabel', + operationType: 'min', + params: { + emptyAsNull: true, + }, + scale: 'ratio', + sourceField: 'timestamp', + }, + }, + incompleteColumns: {}, + indexPatternId: 'first', + }, + }, + }); + }); + it('replace_incompatible: allows dropping to compatible group to replace an existing column', () => { + props = { + ...props, + target: { + ...props.target, + columnId: 'col5', + groupId: 'y', + layerId: 'second', + filterOperations: (op) => !op.isBucketed, + }, + dropType: 'replace_incompatible', + }; + + expect(onDrop(props)).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + first: { + ...mockedLayers.emptyLayer(), + incompleteColumns: {}, + }, + second: { + columnOrder: ['col2', 'col3', 'col4', 'col5'], + columns: { + ...props.state.layers.second.columns, + col5: { + dataType: 'date', + isBucketed: false, + label: 'Minimum of timestampLabel', + operationType: 'min', + params: { + emptyAsNull: true, + }, + scale: 'ratio', + sourceField: 'timestamp', + }, + }, + incompleteColumns: {}, + indexPatternId: 'first', + }, + }, + }); + }); + it('move_incompatible: allows dropping to compatible group to replace an existing column', () => { + props = { + ...props, + target: { + ...props.target, + columnId: 'newCol', + groupId: 'y', + layerId: 'second', + filterOperations: (op) => !op.isBucketed, + }, + dropType: 'move_incompatible', + }; + + expect(onDrop(props)).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + first: { + ...mockedLayers.emptyLayer(), + incompleteColumns: {}, + }, + second: { + columnOrder: ['col2', 'col3', 'col4', 'col5', 'newCol'], + columns: { + ...props.state.layers.second.columns, + newCol: { + dataType: 'date', + isBucketed: false, + label: 'Minimum of timestampLabel', + operationType: 'min', + params: { + emptyAsNull: true, + }, + scale: 'ratio', + sourceField: 'timestamp', + }, + }, + incompleteColumns: {}, + indexPatternId: 'first', + }, + }, + }); + }); + it('duplicate_incompatible: allows dropping to compatible group to replace an existing column', () => { + props = { + ...props, + target: { + ...props.target, + columnId: 'newCol', + groupId: 'y', + layerId: 'second', + filterOperations: (op) => !op.isBucketed, + }, + dropType: 'duplicate_incompatible', + }; + + expect(onDrop(props)).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + second: { + columnOrder: ['col2', 'col3', 'col4', 'col5', 'newCol'], + columns: { + ...props.state.layers.second.columns, + newCol: { + dataType: 'date', + isBucketed: false, + label: 'Minimum of timestampLabel', + operationType: 'min', + params: { + emptyAsNull: true, + }, + scale: 'ratio', + sourceField: 'timestamp', + }, + }, + incompleteColumns: {}, + indexPatternId: 'first', + }, + }, + }); + }); + it('swap_incompatible: allows dropping to compatible group to replace an existing column', () => { + props = { + ...props, + target: { + ...props.target, + columnId: 'col5', + groupId: 'y', + layerId: 'second', + filterOperations: (op) => !op.isBucketed, + }, + dropType: 'swap_incompatible', + }; + + expect(onDrop(props)).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + first: { + ...props.state.layers.first, + columns: { + ...props.state.layers.first.columns, + col1: { + dataType: 'number', + isBucketed: true, + label: 'bytes', + operationType: 'range', + params: { + includeEmptyRows: true, + maxBars: 'auto', + ranges: [ + { + from: 0, + label: '', + to: 1000, + }, + ], + type: 'histogram', + }, + scale: 'interval', + sourceField: 'bytes', + }, + }, + }, + second: { + columnOrder: ['col2', 'col3', 'col4', 'col5'], + columns: { + ...props.state.layers.second.columns, + col5: { + dataType: 'date', + isBucketed: false, + label: 'Minimum of timestampLabel', + operationType: 'min', + params: { + emptyAsNull: true, + }, + scale: 'ratio', + sourceField: 'timestamp', + }, + }, + incompleteColumns: {}, + indexPatternId: 'first', + }, + }, + }); + }); + it('combine_compatible: allows dropping to combine to multiterms', () => { + onDrop({ + ...props, + state: { + ...props.state, + layers: { + ...props.state.layers, + first: { + ...props.state.layers.first, + columns: { + terms1: mockedColumns.terms, + }, + }, + }, + }, + source: { + columnId: 'terms1', + groupId: 'a', + layerId: 'first', + id: 'terms1', + humanData: { label: 'Label' }, + }, + dropType: 'combine_compatible', + target: { + ...props.target, + columnId: 'col4', + groupId: 'a', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + }); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + first: { ...mockedLayers.emptyLayer(), incompleteColumns: {} }, + second: { + ...props.state.layers.second, + incompleteColumns: {}, + columns: { + ...props.state.layers.second.columns, + col4: { + dataType: 'string', + isBucketed: true, + label: 'Top values of dest + 1 other', + operationType: 'terms', + params: { + orderBy: { + type: 'alphabetical', + }, + orderDirection: 'desc', + parentFormat: { + id: 'multi_terms', + }, + secondaryFields: ['src'], + size: 10, + }, + sourceField: 'dest', + }, + }, + }, + }, + }); + }); + it('combine_incompatible: allows dropping to combine to multiterms', () => { + onDrop({ + ...props, + state: { + ...props.state, + layers: { + ...props.state.layers, + first: { + ...props.state.layers.first, + columns: { + median: mockedColumns.median, + }, + }, + }, + }, + source: { + columnId: 'median', + groupId: 'x', + layerId: 'first', + id: 'median', + humanData: { label: 'Label' }, + filterOperations: (op: OperationMetadata) => !op.isBucketed, + }, + dropType: 'combine_incompatible', + target: { + ...props.target, + columnId: 'col4', + groupId: 'breakdown', + filterOperations: (op: OperationMetadata) => op.isBucketed, + }, + }); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + first: { ...mockedLayers.emptyLayer(), incompleteColumns: {} }, + second: { + ...props.state.layers.second, + incompleteColumns: {}, + columns: { + ...props.state.layers.second.columns, + col4: { + dataType: 'string', + isBucketed: true, + label: 'Top values of dest + 1 other', + operationType: 'terms', + params: { + orderBy: { + type: 'alphabetical', + }, + orderDirection: 'desc', + parentFormat: { + id: 'multi_terms', + }, + secondaryFields: ['bytes'], + size: 10, + }, + sourceField: 'dest', + }, + }, + }, + }, + }); + }); + }); + describe('references', () => { + let props: DatasourceDimensionDropHandlerProps; + beforeEach(() => { + props = { + dimensionGroups: defaultDimensionGroups, + setState: jest.fn(), + dropType: 'move_compatible', + + state: { + layers: { + first: { + indexPatternId: 'first', + columns: { + firstColumnX0: { + label: 'Part of count()', + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: '___records___', + customLabel: true, + }, + firstColumn: { + label: 'count()', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { formula: 'count()' }, + references: ['firstColumnX0'], + } as FormulaIndexPatternColumn, + }, + columnOrder: ['firstColumn', 'firstColumnX0'], + incompleteColumns: {}, + }, + second: { + indexPatternId: 'first', + columns: { + secondX0: { + label: 'Part of count()', + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: '___records___', + customLabel: true, + }, + second: { + label: 'count()', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { formula: 'count()' }, + references: ['secondX0'], + } as FormulaIndexPatternColumn, + }, + columnOrder: ['second', 'secondX0'], + }, + }, + indexPatternRefs: [], + indexPatterns: mockDataViews(), + currentIndexPatternId: 'first', + isFirstExistenceFetch: false, + existingFields: { + first: { + timestamp: true, + bytes: true, + memory: true, + source: true, + }, + }, + }, + source: { + columnId: 'firstColumn', + groupId: 'y', + layerId: 'first', + id: 'firstColumn', + humanData: { + label: 'count()', + }, + }, + target: { + columnId: 'newColumn', + groupId: 'y', + layerId: 'second', + filterOperations: (op) => !op.isBucketed, + }, + }; + + jest.clearAllMocks(); + }); + + it('move_compatible; allows dropping to the compatible group in different layer to empty column', () => { + expect(onDrop(props)).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + first: { + ...mockedLayers.emptyLayer(), + incompleteColumns: {}, + }, + second: { + columnOrder: ['second', 'secondX0', 'newColumnX0', 'newColumn'], + columns: { + ...props.state.layers.second.columns, + newColumn: { + dataType: 'number', + isBucketed: false, + label: 'count()', + operationType: 'formula', + params: { + formula: 'count()', + isFormulaBroken: false, + }, + references: ['newColumnX0'], + scale: 'ratio', + }, + newColumnX0: { + customLabel: true, + dataType: 'number', + filter: undefined, + isBucketed: false, + label: 'Part of count()', + operationType: 'count', + params: { + emptyAsNull: false, + }, + scale: 'ratio', + sourceField: '___records___', + timeScale: undefined, + timeShift: undefined, + }, + }, + indexPatternId: 'first', + }, + }, + }); + }); + it('replace_compatible: allows dropping to compatible group to replace an existing column', () => { + expect( + onDrop({ + ...props, + target: { + columnId: 'second', + groupId: 'y', + layerId: 'second', + filterOperations: (op) => !op.isBucketed, + }, + }) + ).toEqual(true); + expect(props.setState).toBeCalledTimes(1); + expect(props.setState).toHaveBeenCalledWith({ + ...props.state, + layers: { + ...props.state.layers, + first: { + ...mockedLayers.emptyLayer(), + incompleteColumns: {}, + }, + second: { + columnOrder: ['second', 'secondX0'], + columns: { + ...props.state.layers.second.columns, + second: { + dataType: 'number', + isBucketed: false, + label: 'count()', + operationType: 'formula', + params: { + formula: 'count()', + isFormulaBroken: false, + }, + references: ['secondX0'], + scale: 'ratio', + }, + secondX0: { + customLabel: true, + dataType: 'number', + filter: undefined, + isBucketed: false, + label: 'Part of count()', + operationType: 'count', + params: { + emptyAsNull: false, + }, + scale: 'ratio', + sourceField: '___records___', + timeScale: undefined, + timeShift: undefined, + }, + }, + indexPatternId: 'first', + }, + }, + }); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts index c9e806050caad..3d57e21e73387 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts @@ -4,7 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { DatasourceDimensionDropHandlerProps, DraggedOperation } from '../../../types'; +import { + DatasourceDimensionDropHandlerProps, + DragDropOperation, + DropType, + isOperation, + StateSetter, + VisualizationDimensionGroupConfig, +} from '../../../types'; import { insertOrReplaceColumn, deleteColumn, @@ -14,160 +21,125 @@ import { hasOperationSupportForMultipleFields, getOperationHelperForMultipleFields, replaceColumn, + deleteColumnInLayers, } from '../../operations'; -import { mergeLayer } from '../../state_helpers'; +import { mergeLayer, mergeLayers } from '../../state_helpers'; import { isDraggedField } from '../../pure_utils'; import { getNewOperation, getField } from './get_drop_props'; -import { IndexPatternPrivateState, DraggedField } from '../../types'; +import { IndexPatternPrivateState, DraggedField, DataViewDragDropOperation } from '../../types'; import { trackUiEvent } from '../../../lens_ui_telemetry'; -type DropHandlerProps = DatasourceDimensionDropHandlerProps & { - droppedItem: T; -}; +interface DropHandlerProps { + state: IndexPatternPrivateState; + setState: StateSetter< + IndexPatternPrivateState, + { + isDimensionComplete?: boolean; + forceRender?: boolean; + } + >; + dimensionGroups: VisualizationDimensionGroupConfig[]; + dropType?: DropType; + source: T; + target: DataViewDragDropOperation; +} export function onDrop(props: DatasourceDimensionDropHandlerProps) { - const { droppedItem, dropType } = props; - - if (dropType === 'field_add' || dropType === 'field_replace' || dropType === 'field_combine') { - return operationOnDropMap[dropType]({ - ...props, - droppedItem: droppedItem as DraggedField, - }); + const { target, source, dropType, state } = props; + + if (isDraggedField(source) && isFieldDropType(dropType)) { + return onFieldDrop( + { + ...props, + target: { + ...target, + dataView: state.indexPatterns[state.layers[target.layerId].indexPatternId], + }, + source, + }, + dropType === 'field_combine' + ); } - return operationOnDropMap[dropType]({ - ...props, - droppedItem: droppedItem as DraggedOperation, - }); -} - -const operationOnDropMap = { - field_add: onFieldDrop, - field_replace: onFieldDrop, - field_combine: (props: DropHandlerProps) => onFieldDrop(props, true), - - reorder: onReorder, - - move_compatible: (props: DropHandlerProps) => onMoveCompatible(props, true), - replace_compatible: (props: DropHandlerProps) => onMoveCompatible(props, true), - duplicate_compatible: onMoveCompatible, - replace_duplicate_compatible: onMoveCompatible, - move_incompatible: (props: DropHandlerProps) => onMoveIncompatible(props, true), - replace_incompatible: (props: DropHandlerProps) => - onMoveIncompatible(props, true), - duplicate_incompatible: onMoveIncompatible, - replace_duplicate_incompatible: onMoveIncompatible, - - swap_compatible: onSwapCompatible, - swap_incompatible: onSwapIncompatible, - combine_compatible: onCombineCompatible, - combine_incompatible: onCombineCompatible, -}; - -function onCombineCompatible({ - columnId, - setState, - state, - layerId, - droppedItem, - dimensionGroups, - groupId, -}: DropHandlerProps) { - const layer = state.layers[layerId]; - const sourceId = droppedItem.columnId; - const targetId = columnId; - const indexPattern = state.indexPatterns[layer.indexPatternId]; - const sourceColumn = layer.columns[sourceId]; - const targetColumn = layer.columns[targetId]; - - // extract the field from the source column - const sourceField = getField(sourceColumn, indexPattern); - const targetField = getField(targetColumn, indexPattern); - if (!sourceField || !targetField) { + if (!isOperation(source)) { + return false; + } + const sourceDataView = state.indexPatterns[state.layers[source.layerId].indexPatternId]; + const targetDataView = state.indexPatterns[state.layers[target.layerId].indexPatternId]; + if (sourceDataView !== targetDataView) { return false; } - // pass it to the target column and delete the source column - const initialParams = { - params: - getOperationHelperForMultipleFields(targetColumn.operationType)?.({ - targetColumn, - sourceColumn, - indexPattern, - }) ?? {}, - }; - - const modifiedLayer = replaceColumn({ - layer, - columnId, - indexPattern, - op: targetColumn.operationType, - field: targetField, - visualizationGroups: dimensionGroups, - targetGroup: groupId, - initialParams, - shouldCombineField: true, - }); - const newLayer = deleteColumn({ - layer: modifiedLayer, - columnId: sourceId, - indexPattern, - }); - // Time to replace - setState( - mergeLayer({ - state, - layerId, - newLayer, - }) - ); + const operationProps = { + ...props, + target: { + ...target, + dataView: targetDataView, + }, + source: { + ...source, + dataView: sourceDataView, + }, + }; + if (dropType === 'reorder') { + return onReorder(operationProps); + } - return { deleted: sourceId }; + if (['move_compatible', 'replace_compatible'].includes(dropType)) { + return onMoveCompatible(operationProps, true); + } + if (['duplicate_compatible', 'replace_duplicate_compatible'].includes(dropType)) { + return onMoveCompatible(operationProps); + } + if (['move_incompatible', 'replace_incompatible'].includes(dropType)) { + return onMoveIncompatible(operationProps, true); + } + if (['duplicate_incompatible', 'replace_duplicate_incompatible'].includes(dropType)) { + return onMoveIncompatible(operationProps); + } + if (dropType === 'swap_compatible') { + return onSwapCompatible(operationProps); + } + if (dropType === 'swap_incompatible') { + return onSwapIncompatible(operationProps); + } + if (['combine_incompatible', 'combine_compatible'].includes(dropType)) { + return onCombine(operationProps); + } } +const isFieldDropType = (dropType: DropType) => + ['field_add', 'field_replace', 'field_combine'].includes(dropType); + function onFieldDrop(props: DropHandlerProps, shouldAddField?: boolean) { - const { - columnId, - setState, - state, - layerId, - droppedItem, - filterOperations, - groupId, - dimensionGroups, - } = props; + const { setState, state, source, target, dimensionGroups } = props; const prioritizedOperation = dimensionGroups.find( - (g) => g.groupId === groupId + (g) => g.groupId === target.groupId )?.prioritizedOperation; - const layer = state.layers[layerId]; + const layer = state.layers[target.layerId]; const indexPattern = state.indexPatterns[layer.indexPatternId]; - const targetColumn = layer.columns[columnId]; + const targetColumn = layer.columns[target.columnId]; const newOperation = shouldAddField ? targetColumn.operationType - : getNewOperation(droppedItem.field, filterOperations, targetColumn, prioritizedOperation); + : getNewOperation(source.field, target.filterOperations, targetColumn, prioritizedOperation); if ( - !isDraggedField(droppedItem) || + !isDraggedField(source) || !newOperation || (shouldAddField && - !hasOperationSupportForMultipleFields( - indexPattern, - targetColumn, - undefined, - droppedItem.field - )) + !hasOperationSupportForMultipleFields(indexPattern, targetColumn, undefined, source.field)) ) { return false; } - const field = shouldAddField ? getField(targetColumn, indexPattern) : droppedItem.field; + const field = shouldAddField ? getField(targetColumn, indexPattern) : source.field; const initialParams = shouldAddField ? { params: getOperationHelperForMultipleFields(targetColumn.operationType)?.({ targetColumn, - field: droppedItem.field, + field: source.field, indexPattern, }) || {}, } @@ -175,12 +147,12 @@ function onFieldDrop(props: DropHandlerProps, shouldAddField?: boo const newLayer = insertOrReplaceColumn({ layer, - columnId, + columnId: target.columnId, indexPattern, op: newOperation, field, visualizationGroups: dimensionGroups, - targetGroup: groupId, + targetGroup: target.groupId, shouldCombineField: shouldAddField, initialParams, }); @@ -188,82 +160,76 @@ function onFieldDrop(props: DropHandlerProps, shouldAddField?: boo trackUiEvent('drop_onto_dimension'); const hasData = Object.values(state.layers).some(({ columns }) => columns.length); trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty'); - setState(mergeLayer({ state, layerId, newLayer })); + setState(mergeLayer({ state, layerId: target.layerId, newLayer })); return true; } function onMoveCompatible( - { - columnId, - setState, - state, - layerId, - droppedItem, - dimensionGroups, - groupId, - }: DropHandlerProps, + { setState, state, source, target, dimensionGroups }: DropHandlerProps, shouldDeleteSource?: boolean ) { - const layer = state.layers[layerId]; - const sourceColumn = layer.columns[droppedItem.columnId]; - const indexPattern = state.indexPatterns[layer.indexPatternId]; - - const modifiedLayer = copyColumn({ - layer, - targetId: columnId, - sourceColumnId: droppedItem.columnId, - sourceColumn, + const modifiedLayers = copyColumn({ + layers: state.layers, + target, + source, shouldDeleteSource, - indexPattern, }); - const updatedColumnOrder = reorderByGroups( - dimensionGroups, - groupId, - getColumnOrder(modifiedLayer), - columnId - ); + if (target.layerId === source.layerId) { + const updatedColumnOrder = reorderByGroups( + dimensionGroups, + getColumnOrder(modifiedLayers[target.layerId]), + target.groupId, + target.columnId + ); + + const newLayer = { + ...modifiedLayers[target.layerId], + columnOrder: updatedColumnOrder, + columns: modifiedLayers[target.layerId].columns, + }; + + // Time to replace + setState( + mergeLayer({ + state, + layerId: target.layerId, + newLayer, + }) + ); + return true; + } else { + setState(mergeLayers({ state, newLayers: modifiedLayers })); - // Time to replace - setState( - mergeLayer({ - state, - layerId, - newLayer: { - columnOrder: updatedColumnOrder, - columns: modifiedLayer.columns, - }, - }) - ); - return shouldDeleteSource ? { deleted: droppedItem.columnId } : true; + return true; + } } function onReorder({ - columnId, setState, state, - layerId, - droppedItem, -}: DropHandlerProps) { - function reorderElements(items: string[], dest: string, src: string) { - const result = items.filter((c) => c !== src); - const targetIndex = items.findIndex((c) => c === src); - const sourceIndex = items.findIndex((c) => c === dest); - - const targetPosition = result.indexOf(dest); - result.splice(targetIndex < sourceIndex ? targetPosition + 1 : targetPosition, 0, src); + source, + target, +}: DropHandlerProps) { + function reorderElements(items: string[], targetId: string, sourceId: string) { + const result = items.filter((c) => c !== sourceId); + const targetIndex = items.findIndex((c) => c === sourceId); + const sourceIndex = items.findIndex((c) => c === targetId); + + const targetPosition = result.indexOf(targetId); + result.splice(targetIndex < sourceIndex ? targetPosition + 1 : targetPosition, 0, sourceId); return result; } setState( mergeLayer({ state, - layerId, + layerId: target.layerId, newLayer: { columnOrder: reorderElements( - state.layers[layerId].columnOrder, - columnId, - droppedItem.columnId + state.layers[target.layerId].columnOrder, + target.columnId, + source.columnId ), }, }) @@ -272,124 +238,158 @@ function onReorder({ } function onMoveIncompatible( - { - columnId, - setState, - state, - layerId, - droppedItem, - filterOperations, - dimensionGroups, - groupId, - }: DropHandlerProps, + { setState, state, source, dimensionGroups, target }: DropHandlerProps, shouldDeleteSource?: boolean ) { - const layer = state.layers[layerId]; - const indexPattern = state.indexPatterns[layer.indexPatternId]; - const sourceColumn = layer.columns[droppedItem.columnId]; - const targetColumn = layer.columns[columnId] || null; - + const targetLayer = state.layers[target.layerId]; + const targetColumn = targetLayer.columns[target.columnId] || null; + const sourceLayer = state.layers[source.layerId]; + const indexPattern = state.indexPatterns[sourceLayer.indexPatternId]; + const sourceColumn = sourceLayer.columns[source.columnId]; const sourceField = getField(sourceColumn, indexPattern); - const newOperation = getNewOperation(sourceField, filterOperations, targetColumn); + const newOperation = getNewOperation(sourceField, target.filterOperations, targetColumn); if (!newOperation) { return false; } - const modifiedLayer = shouldDeleteSource + const outputSourceLayer = shouldDeleteSource ? deleteColumn({ - layer, - columnId: droppedItem.columnId, + layer: sourceLayer, + columnId: source.columnId, indexPattern, }) - : layer; + : sourceLayer; - const newLayer = insertOrReplaceColumn({ - layer: modifiedLayer, - columnId, - indexPattern, - op: newOperation, - field: sourceField, - visualizationGroups: dimensionGroups, - targetGroup: groupId, - shouldResetLabel: true, - }); + if (target.layerId === source.layerId) { + const newLayer = insertOrReplaceColumn({ + layer: outputSourceLayer, + columnId: target.columnId, + indexPattern, + op: newOperation, + field: sourceField, + visualizationGroups: dimensionGroups, + targetGroup: target.groupId, + shouldResetLabel: true, + }); - trackUiEvent('drop_onto_dimension'); - setState( - mergeLayer({ - state, - layerId, - newLayer, - }) - ); - return shouldDeleteSource ? { deleted: droppedItem.columnId } : true; + trackUiEvent('drop_onto_dimension'); + setState( + mergeLayer({ + state, + layerId: target.layerId, + newLayer, + }) + ); + return true; + } else { + const outputTargetLayer = insertOrReplaceColumn({ + layer: targetLayer, + columnId: target.columnId, + indexPattern, + op: newOperation, + field: sourceField, + visualizationGroups: dimensionGroups, + targetGroup: target.groupId, + shouldResetLabel: true, + }); + + trackUiEvent('drop_onto_dimension'); + setState( + mergeLayers({ + state, + newLayers: { + [source.layerId]: outputSourceLayer, + [target.layerId]: outputTargetLayer, + }, + }) + ); + return true; + } } function onSwapIncompatible({ - columnId, setState, state, - layerId, - droppedItem, - filterOperations, + source, dimensionGroups, - groupId, -}: DropHandlerProps) { - const layer = state.layers[layerId]; - const indexPattern = state.indexPatterns[layer.indexPatternId]; - const sourceColumn = layer.columns[droppedItem.columnId]; - const targetColumn = layer.columns[columnId]; + target, +}: DropHandlerProps) { + const targetLayer = state.layers[target.layerId]; + const sourceLayer = state.layers[source.layerId]; + const indexPattern = state.indexPatterns[targetLayer.indexPatternId]; + const sourceColumn = sourceLayer.columns[source.columnId]; + const targetColumn = targetLayer.columns[target.columnId]; const sourceField = getField(sourceColumn, indexPattern); const targetField = getField(targetColumn, indexPattern); - const newOperationForSource = getNewOperation(sourceField, filterOperations, targetColumn); - const newOperationForTarget = getNewOperation( - targetField, - droppedItem.filterOperations, - sourceColumn - ); + const newOperationForSource = getNewOperation(sourceField, target.filterOperations, targetColumn); + const newOperationForTarget = getNewOperation(targetField, source.filterOperations, sourceColumn); if (!newOperationForSource || !newOperationForTarget) { return false; } - const newLayer = insertOrReplaceColumn({ - layer: insertOrReplaceColumn({ - layer, - columnId, - targetGroup: groupId, - indexPattern, - op: newOperationForSource, - field: sourceField, - visualizationGroups: dimensionGroups, - shouldResetLabel: true, - }), - columnId: droppedItem.columnId, + const outputTargetLayer = insertOrReplaceColumn({ + layer: targetLayer, + columnId: target.columnId, + targetGroup: target.groupId, indexPattern, - op: newOperationForTarget, - field: targetField, + op: newOperationForSource, + field: sourceField, visualizationGroups: dimensionGroups, - targetGroup: droppedItem.groupId, shouldResetLabel: true, }); - trackUiEvent('drop_onto_dimension'); - setState( - mergeLayer({ - state, - layerId, - newLayer, - }) - ); - return true; + if (source.layerId === target.layerId) { + const newLayer = insertOrReplaceColumn({ + layer: outputTargetLayer, + columnId: source.columnId, + indexPattern, + op: newOperationForTarget, + field: targetField, + visualizationGroups: dimensionGroups, + targetGroup: source.groupId, + shouldResetLabel: true, + }); + + trackUiEvent('drop_onto_dimension'); + setState( + mergeLayer({ + state, + layerId: target.layerId, + newLayer, + }) + ); + return true; + } else { + const outputSourceLayer = insertOrReplaceColumn({ + layer: sourceLayer, + columnId: source.columnId, + indexPattern, + op: newOperationForTarget, + field: targetField, + visualizationGroups: dimensionGroups, + targetGroup: source.groupId, + shouldResetLabel: true, + }); + + trackUiEvent('drop_onto_dimension'); + setState( + mergeLayers({ + state, + newLayers: { [source.layerId]: outputSourceLayer, [target.layerId]: outputTargetLayer }, + }) + ); + return true; + } } const swapColumnOrder = (columnOrder: string[], sourceId: string, targetId: string) => { - const newColumnOrder = [...columnOrder]; - const sourceIndex = newColumnOrder.findIndex((c) => c === sourceId); - const targetIndex = newColumnOrder.findIndex((c) => c === targetId); + const sourceIndex = columnOrder.findIndex((c) => c === sourceId); + const targetIndex = columnOrder.findIndex((c) => c === targetId); + const newColumnOrder = [...columnOrder]; newColumnOrder[sourceIndex] = targetId; newColumnOrder[targetIndex] = sourceId; @@ -397,38 +397,114 @@ const swapColumnOrder = (columnOrder: string[], sourceId: string, targetId: stri }; function onSwapCompatible({ - columnId, setState, state, - layerId, - droppedItem, + source, dimensionGroups, - groupId, -}: DropHandlerProps) { - const layer = state.layers[layerId]; - const sourceId = droppedItem.columnId; - const targetId = columnId; - - const sourceColumn = { ...layer.columns[sourceId] }; - const targetColumn = { ...layer.columns[targetId] }; - const newColumns = { ...layer.columns }; - newColumns[targetId] = sourceColumn; - newColumns[sourceId] = targetColumn; - - let updatedColumnOrder = swapColumnOrder(layer.columnOrder, sourceId, targetId); - updatedColumnOrder = reorderByGroups(dimensionGroups, groupId, updatedColumnOrder, columnId); - - // Time to replace - setState( - mergeLayer({ - state, - layerId, - newLayer: { - columnOrder: updatedColumnOrder, - columns: newColumns, - }, - }) - ); + target, +}: DropHandlerProps) { + if (target.layerId === source.layerId) { + const layer = state.layers[target.layerId]; + const newColumns = { + ...layer.columns, + [target.columnId]: { ...layer.columns[source.columnId] }, + [source.columnId]: { ...layer.columns[target.columnId] }, + }; + + let updatedColumnOrder = swapColumnOrder(layer.columnOrder, source.columnId, target.columnId); + updatedColumnOrder = reorderByGroups( + dimensionGroups, + updatedColumnOrder, + target.groupId, + target.columnId + ); + + setState( + mergeLayer({ + state, + layerId: target.layerId, + newLayer: { + columnOrder: updatedColumnOrder, + columns: newColumns, + }, + }) + ); + + return true; + } else { + const newTargetLayer = copyColumn({ + layers: state.layers, + target, + source, + shouldDeleteSource: true, + })[target.layerId]; + + const newSourceLayer = copyColumn({ + layers: state.layers, + target: source, + source: target, + shouldDeleteSource: true, + })[source.layerId]; + + setState( + mergeLayers({ + state, + newLayers: { + [source.layerId]: newSourceLayer, + [target.layerId]: newTargetLayer, + }, + }) + ); + + return true; + } +} + +function onCombine({ + state, + setState, + source, + target, + dimensionGroups, +}: DropHandlerProps) { + const targetLayer = state.layers[target.layerId]; + const targetColumn = targetLayer.columns[target.columnId]; + const targetField = getField(targetColumn, target.dataView); + const indexPattern = state.indexPatterns[targetLayer.indexPatternId]; + + const sourceLayer = state.layers[source.layerId]; + const sourceColumn = sourceLayer.columns[source.columnId]; + const sourceField = getField(sourceColumn, indexPattern); + // extract the field from the source column + if (!sourceField || !targetField) { + return false; + } + // pass it to the target column and delete the source column + const initialParams = { + params: + getOperationHelperForMultipleFields(targetColumn.operationType)?.({ + targetColumn, + sourceColumn, + indexPattern, + }) ?? {}, + }; + const outputTargetLayer = replaceColumn({ + layer: targetLayer, + columnId: target.columnId, + indexPattern, + op: targetColumn.operationType, + field: targetField, + visualizationGroups: dimensionGroups, + targetGroup: target.groupId, + initialParams, + shouldCombineField: true, + }); + + const newLayers = deleteColumnInLayers({ + layers: { ...state.layers, [target.layerId]: outputTargetLayer }, + source, + }); + setState(mergeLayers({ state, newLayers })); return true; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts index 7a7297e77bcf2..2f703547219ec 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts @@ -18,9 +18,9 @@ export interface OperationSupportMatrix { } type Props = Pick< - DatasourceDimensionDropProps, - 'layerId' | 'columnId' | 'state' | 'filterOperations' ->; + DatasourceDimensionDropProps['target'], + 'layerId' | 'columnId' | 'filterOperations' +> & { state: IndexPatternPrivateState }; function computeOperationMatrix( operationsByMetadata: Array<{ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx index 437ee4bf8b22d..104b85651a876 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx @@ -150,12 +150,8 @@ export const countOperation: OperationDefinition - adjustTimeScaleOnOtherColumnChange( - layer, - thisColumnId, - changedColumnId - ), + onOtherColumnChanged: (layer, thisColumnId) => + adjustTimeScaleOnOtherColumnChange(layer, thisColumnId), toEsAggsFn: (column, columnId) => { return buildExpressionFunction('aggCount', { id: columnId, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx index 1d08873a160e9..72aace21479ac 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx @@ -174,13 +174,23 @@ export const formulaOperation: OperationDefinition { return true; }, - createCopy(layer, sourceId, targetId, indexPattern, operationDefinitionMap) { - const currentColumn = layer.columns[sourceId] as FormulaIndexPatternColumn; - - return insertOrReplaceFormulaColumn(targetId, currentColumn, layer, { - indexPattern, - operations: operationDefinitionMap, - }).layer; + createCopy(layers, source, target, operationDefinitionMap) { + const currentColumn = layers[source.layerId].columns[ + source.columnId + ] as FormulaIndexPatternColumn; + const modifiedLayer = insertOrReplaceFormulaColumn( + target.columnId, + currentColumn, + layers[target.layerId], + { + indexPattern: target.dataView, + operations: operationDefinitionMap, + } + ); + return { + ...layers, + [target.layerId]: modifiedLayer.layer, + }; }, timeScalingMode: 'optional', paramEditor: WrappedFormulaEditor, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx index 85c2ea707b123..d7f25275f63a2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx @@ -67,8 +67,8 @@ export const mathOperation: OperationDefinition { - return { ...layer }; + createCopy: (layers) => { + return { ...layers }; }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 74d635cac02dc..cdf2b0249529e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -54,7 +54,12 @@ import type { GenericIndexPatternColumn, ReferenceBasedIndexPatternColumn, } from './column_types'; -import { IndexPattern, IndexPatternField, IndexPatternLayer } from '../../types'; +import { + DataViewDragDropOperation, + IndexPattern, + IndexPatternField, + IndexPatternLayer, +} from '../../types'; import { DateRange, LayerType } from '../../../../common'; import { rangeOperation } from './ranges'; import { IndexPatternDimensionEditorProps, OperationSupportMatrix } from '../../dimension_panel'; @@ -249,11 +254,7 @@ interface BaseOperationDefinitionProps * Based on the current column and the other updated columns, this function has to * return an updated column. If not implemented, the `id` function is used instead. */ - onOtherColumnChanged?: ( - layer: IndexPatternLayer, - thisColumnId: string, - changedColumnId: string - ) => C; + onOtherColumnChanged?: (layer: IndexPatternLayer, thisColumnId: string) => C; /** * React component for operation specific settings shown in the flyout editor */ @@ -623,12 +624,11 @@ interface ManagedReferenceOperationDefinition * root level */ createCopy: ( - layer: IndexPatternLayer, - sourceColumnId: string, - targetColumnId: string, - indexPattern: IndexPattern, + layers: Record, + source: DataViewDragDropOperation, + target: DataViewDragDropOperation, operationDefinitionMap: Record - ) => IndexPatternLayer; + ) => Record; } interface OperationDefinitionMap { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index 55df2b8e0ff1a..10c4310e820b2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -108,9 +108,9 @@ function buildMetricOperation>({ (!newField.aggregationRestrictions || newField.aggregationRestrictions![type]) ); }, - onOtherColumnChanged: (layer, thisColumnId, changedColumnId) => + onOtherColumnChanged: (layer, thisColumnId) => optionalTimeScaling - ? (adjustTimeScaleOnOtherColumnChange(layer, thisColumnId, changedColumnId) as T) + ? (adjustTimeScaleOnOtherColumnChange(layer, thisColumnId) as T) : (layer.columns[thisColumnId] as T), getDefaultLabel: (column, indexPattern, columns) => labelLookup(getSafeName(column.sourceField, indexPattern), column), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx index 555360a2f7f6c..5642c06c6b642 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx @@ -16,6 +16,7 @@ import { import type { IndexPattern } from '../../types'; import { useDebouncedValue } from '../../../shared_components'; import { getFormatFromPreviousColumn, isValidNumber } from './helpers'; +import { getColumnOrder } from '../layer_helpers'; const defaultLabel = i18n.translate('xpack.lens.indexPattern.staticValueLabelDefault', { defaultMessage: 'Static value', @@ -132,13 +133,21 @@ export const staticValueOperation: OperationDefinition< isTransferable: (column) => { return true; }, - createCopy(layer, sourceId, targetId, indexPattern, operationDefinitionMap) { - const currentColumn = layer.columns[sourceId] as StaticValueIndexPatternColumn; + createCopy(layers, source, target) { + const currentColumn = layers[source.layerId].columns[ + source.columnId + ] as StaticValueIndexPatternColumn; + const targetLayer = layers[target.layerId]; + const columns = { + ...targetLayer.columns, + [target.columnId]: { ...currentColumn }, + }; return { - ...layer, - columns: { - ...layer.columns, - [targetId]: { ...currentColumn }, + ...layers, + [target.layerId]: { + ...targetLayer, + columns, + columnOrder: getColumnOrder({ ...targetLayer, columns }), }, }; }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index 419e087411810..62aed475df42a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -325,7 +325,7 @@ export const termsOperation: OperationDefinition { + onOtherColumnChanged: (layer, thisColumnId) => { const columns = layer.columns; const currentColumn = columns[thisColumnId] as TermsIndexPatternColumn; if ( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index 1cce6c5b06cd6..99c20bbd8bca6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -755,8 +755,7 @@ describe('terms', () => { }, }, }, - 'col2', - 'col1' + 'col2' ); expect(updatedColumn).toBe(initialColumn); @@ -796,8 +795,7 @@ describe('terms', () => { columnOrder: [], indexPatternId: '', }, - 'col2', - 'col1' + 'col2' ); expect(updatedColumn.params).toEqual( expect.objectContaining({ @@ -843,8 +841,7 @@ describe('terms', () => { columnOrder: [], indexPatternId: '', }, - 'col2', - 'col1' + 'col2' ); expect(updatedColumn.params).toEqual( expect.objectContaining({ @@ -875,8 +872,7 @@ describe('terms', () => { columnOrder: [], indexPatternId: '', }, - 'col2', - 'col1' + 'col2' ); expect(termsColumn.params).toEqual( expect.objectContaining({ @@ -919,8 +915,7 @@ describe('terms', () => { columnOrder: [], indexPatternId: '', }, - 'col2', - 'col1' + 'col2' ); expect(termsColumn.params).toEqual( expect.objectContaining({ @@ -951,8 +946,7 @@ describe('terms', () => { columnOrder: [], indexPatternId: '', }, - 'col2', - 'col1' + 'col2' ); expect(termsColumn.params).toEqual( expect.objectContaining({ @@ -991,8 +985,7 @@ describe('terms', () => { }, }, }, - 'col2', - 'col1' + 'col2' ); expect(updatedColumn.params).toEqual( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index 4fdd82439fc22..ab9319957afca 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -159,24 +159,38 @@ describe('state_helpers', () => { params: { window: 5 }, references: ['formulaX0'], }; + expect( copyColumn({ - layer: { - indexPatternId: '', - columnOrder: [], - columns: { - source, - formulaX0: sum, - formulaX1: movingAvg, - formulaX2: math, + layers: { + layer: { + indexPatternId: '', + columnOrder: [], + columns: { + source, + formulaX0: sum, + formulaX1: movingAvg, + formulaX2: math, + }, }, }, - targetId: 'copy', - sourceColumn: source, + source: { + column: source, + groupId: 'one', + columnId: 'source', + layerId: 'layer', + dataView: indexPattern, + filterOperations: () => true, + }, + target: { + columnId: 'copy', + groupId: 'one', + dataView: indexPattern, + layerId: 'layer', + filterOperations: () => true, + }, shouldDeleteSource: false, - indexPattern, - sourceColumnId: 'source', - }) + }).layer ).toEqual({ indexPatternId: '', columnOrder: [ @@ -1355,8 +1369,7 @@ describe('state_helpers', () => { }, incompleteColumns: {}, }, - 'col1', - 'col2' + 'col1' ); }); @@ -1422,8 +1435,7 @@ describe('state_helpers', () => { }, incompleteColumns: {}, }), - 'col1', - 'willBeReference' + 'col1' ); }); @@ -2374,8 +2386,7 @@ describe('state_helpers', () => { expect(operationDefinitionMap.terms.onOtherColumnChanged).toHaveBeenCalledWith( { indexPatternId: '1', columnOrder: ['col1', 'col2'], columns: { col1: termsColumn } }, - 'col1', - 'col2' + 'col1' ); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 8376a57ddc19d..434370943fbc1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -26,6 +26,7 @@ import { TermsIndexPatternColumn, } from './definitions'; import type { + DataViewDragDropOperation, IndexPattern, IndexPatternField, IndexPatternLayer, @@ -68,96 +69,84 @@ interface ColumnChange { } interface ColumnCopy { - layer: IndexPatternLayer; - targetId: string; - sourceColumn: GenericIndexPatternColumn; - sourceColumnId: string; - indexPattern: IndexPattern; + layers: Record; + target: DataViewDragDropOperation; + source: DataViewDragDropOperation; shouldDeleteSource?: boolean; } +export const deleteColumnInLayers = ({ + layers, + source, +}: { + layers: Record; + source: DataViewDragDropOperation; +}) => ({ + ...layers, + [source.layerId]: deleteColumn({ + layer: layers[source.layerId], + columnId: source.columnId, + indexPattern: source.dataView, + }), +}); + export function copyColumn({ - layer, - targetId, - sourceColumn, + layers, + source, + target, shouldDeleteSource, - indexPattern, - sourceColumnId, -}: ColumnCopy): IndexPatternLayer { - let modifiedLayer = copyReferencesRecursively( - layer, - sourceColumn, - sourceColumnId, - targetId, - indexPattern - ); - - if (shouldDeleteSource) { - modifiedLayer = deleteColumn({ - layer: modifiedLayer, - columnId: sourceColumnId, - indexPattern, - }); - } - - return modifiedLayer; +}: ColumnCopy): Record { + const outputLayers = createCopiedColumn(layers, target, source); + return shouldDeleteSource + ? deleteColumnInLayers({ + layers: outputLayers, + source, + }) + : outputLayers; } -function copyReferencesRecursively( - layer: IndexPatternLayer, - sourceColumn: GenericIndexPatternColumn, - sourceId: string, - targetId: string, - indexPattern: IndexPattern -): IndexPatternLayer { - let columns = { ...layer.columns }; +function createCopiedColumn( + layers: Record, + target: DataViewDragDropOperation, + source: DataViewDragDropOperation +): Record { + const sourceLayer = layers[source.layerId]; + const sourceColumn = sourceLayer.columns[source.columnId]; + const targetLayer = layers[target.layerId]; + let columns = { ...targetLayer.columns }; if ('references' in sourceColumn) { - if (columns[targetId]) { - return layer; - } - const def = operationDefinitionMap[sourceColumn.operationType]; if ('createCopy' in def) { - // Allow managed references to recursively insert new columns - return def.createCopy(layer, sourceId, targetId, indexPattern, operationDefinitionMap); + return def.createCopy(layers, source, target, operationDefinitionMap); // Allow managed references to recursively insert new columns } + const referenceColumns = sourceColumn.references.reduce((refs, sourceRef) => { + const newRefId = generateId(); + return { ...refs, [newRefId]: { ...sourceLayer.columns[sourceRef] } }; + }, {}); - sourceColumn?.references.forEach((ref, index) => { - const newId = generateId(); - const refColumn = { ...columns[ref] }; - - // TODO: For fullReference types, now all references are hidden columns, - // but in the future we will have references to visible columns - // and visible columns shouldn't be copied - const refColumnWithInnerRefs = - 'references' in refColumn - ? copyReferencesRecursively(layer, refColumn, sourceId, newId, indexPattern).columns // if a column has references, copy them too - : { [newId]: refColumn }; - - const newColumn = columns[targetId]; - let references = [newId]; - if (newColumn && 'references' in newColumn) { - references = newColumn.references; - references[index] = newId; - } - - columns = { - ...columns, - ...refColumnWithInnerRefs, - [targetId]: { - ...sourceColumn, - references, - }, - }; - }); + columns = { + ...columns, + ...referenceColumns, + [target.columnId]: { + ...sourceColumn, + references: Object.keys(referenceColumns), + }, + }; } else { columns = { ...columns, - [targetId]: sourceColumn, + [target.columnId]: { ...sourceColumn }, }; } - return { ...layer, columns, columnOrder: getColumnOrder({ ...layer, columns }) }; + return { + ...layers, + [target.layerId]: adjustColumnReferences({ + ...targetLayer, + columns, + columnOrder: getColumnOrder({ ...targetLayer, columns }), + }), + }; } export function insertOrReplaceColumn(args: ColumnChange): IndexPatternLayer { @@ -1046,8 +1035,8 @@ function addBucket( } updatedColumnOrder = reorderByGroups( visualizationGroups, - targetGroup, updatedColumnOrder, + targetGroup, addedColumnId ); const tempLayer = { @@ -1064,8 +1053,8 @@ function addBucket( export function reorderByGroups( visualizationGroups: VisualizationDimensionGroupConfig[], - targetGroup: string | undefined, updatedColumnOrder: string[], + targetGroup: string | undefined, addedColumnId: string ) { const hidesColumnGrouping = @@ -1184,6 +1173,26 @@ export function updateColumnParam({ }; } +export function adjustColumnReferences(layer: IndexPatternLayer) { + const newColumns = { ...layer.columns }; + Object.keys(newColumns).forEach((currentColumnId) => { + const currentColumn = newColumns[currentColumnId]; + if (currentColumn?.operationType) { + const operationDefinition = operationDefinitionMap[currentColumn.operationType]; + newColumns[currentColumnId] = operationDefinition.onOtherColumnChanged + ? operationDefinition.onOtherColumnChanged( + { ...layer, columns: newColumns }, + currentColumnId + ) + : currentColumn; + } + }); + return { + ...layer, + columns: newColumns, + }; +} + export function adjustColumnReferencesForChangedColumn( layer: IndexPatternLayer, changedColumnId: string @@ -1196,8 +1205,7 @@ export function adjustColumnReferencesForChangedColumn( newColumns[currentColumnId] = operationDefinition.onOtherColumnChanged ? operationDefinition.onOtherColumnChanged( { ...layer, columns: newColumns }, - currentColumnId, - changedColumnId + currentColumnId ) : currentColumn; } @@ -1561,6 +1569,9 @@ export function isColumnValidAsReference({ if (!column) return false; const operationType = column.operationType; const operationDefinition = operationDefinitionMap[operationType]; + if (!operationDefinition) { + throw new Error('No suitable operation definition found for ' + operationType); + } return ( validation.input.includes(operationDefinition.input) && maybeValidateOperations({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.test.ts index 1eb02fa82ceef..fd8952ad9e077 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.test.ts @@ -113,11 +113,7 @@ describe('time scale utils', () => { it('should keep column if there is no time scale', () => { const column = { ...baseColumn, timeScale: undefined }; expect( - adjustTimeScaleOnOtherColumnChange( - { ...baseLayer, columns: { col1: column } }, - 'col1', - 'col2' - ) + adjustTimeScaleOnOtherColumnChange({ ...baseLayer, columns: { col1: column } }, 'col1') ).toBe(column); }); @@ -138,14 +134,13 @@ describe('time scale utils', () => { } as DateHistogramIndexPatternColumn, }, }, - 'col1', - 'col2' + 'col1' ) ).toBe(baseColumn); }); it('should remove time scale if there is no date histogram', () => { - expect(adjustTimeScaleOnOtherColumnChange(baseLayer, 'col1', 'col2')).toHaveProperty( + expect(adjustTimeScaleOnOtherColumnChange(baseLayer, 'col1')).toHaveProperty( 'timeScale', undefined ); @@ -153,22 +148,14 @@ describe('time scale utils', () => { it('should remove suffix from label', () => { expect( - adjustTimeScaleOnOtherColumnChange( - { ...baseLayer, columns: { col1: baseColumn } }, - 'col1', - 'col2' - ) + adjustTimeScaleOnOtherColumnChange({ ...baseLayer, columns: { col1: baseColumn } }, 'col1') ).toHaveProperty('label', 'Count of records'); }); it('should keep custom label', () => { const column = { ...baseColumn, label: 'abc', customLabel: true }; expect( - adjustTimeScaleOnOtherColumnChange( - { ...baseLayer, columns: { col1: column } }, - 'col1', - 'col2' - ) + adjustTimeScaleOnOtherColumnChange({ ...baseLayer, columns: { col1: column } }, 'col1') ).toHaveProperty('label', 'abc'); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.ts index a8e71c0fd86e5..c6cd343504253 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.ts @@ -46,8 +46,7 @@ export function adjustTimeScaleLabelSuffix( export function adjustTimeScaleOnOtherColumnChange( layer: IndexPatternLayer, - thisColumnId: string, - changedColumnId: string + thisColumnId: string ): T { const columns = layer.columns; const column = columns[thisColumnId] as T; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts index 5d48922a66d8a..6e16ebe5e8d53 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.ts @@ -24,3 +24,19 @@ export function mergeLayer({ }, }; } + +export function mergeLayers({ + state, + newLayers, +}: { + state: IndexPatternPrivateState; + newLayers: Record; +}) { + return { + ...state, + layers: { + ...state.layers, + ...newLayers, + }, + }; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index 7e25509c3b2dd..cbf02bddb8814 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -10,6 +10,7 @@ import type { FieldSpec } from '@kbn/data-plugin/common'; import type { FieldFormatParams } from '@kbn/field-formats-plugin/common'; import type { DragDropIdentifier } from '../drag_drop/providers'; import type { IncompleteColumn, GenericIndexPatternColumn } from './operations'; +import { DragDropOperation } from '../types'; export type { GenericIndexPatternColumn, @@ -109,3 +110,8 @@ export interface IndexPatternRef { title: string; name?: string; } + +export interface DataViewDragDropOperation extends DragDropOperation { + dataView: IndexPattern; + column?: GenericIndexPatternColumn; +} diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 873159562dc8f..770f4bee7eecd 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -199,11 +199,18 @@ interface ChartSettings { }; } -export type GetDropProps = DatasourceDimensionDropProps & { - groupId: string; - dragging: DragContextState['dragging']; - prioritizedOperation?: string; -}; +export interface GetDropPropsArgs { + state: T; + source?: DraggingIdentifier; + target: { + layerId: string; + groupId: string; + columnId: string; + filterOperations: (meta: OperationMetadata) => boolean; + prioritizedOperation?: string; + isNewColumn?: boolean; + }; +} /** * Interface for the datasource registry @@ -257,9 +264,9 @@ export interface Datasource { props: DatasourceLayerPanelProps ) => ((cleanupElement: Element) => void) | void; getDropProps: ( - props: GetDropProps + props: GetDropPropsArgs ) => { dropTypes: DropType[]; nextLabel?: string } | undefined; - onDrop: (props: DatasourceDimensionDropHandlerProps) => false | true | { deleted: string }; + onDrop: (props: DatasourceDimensionDropHandlerProps) => boolean | undefined; /** * The datasource is allowed to cancel a close event on the dimension editor, * mainly used for formulas @@ -454,16 +461,14 @@ export interface DatasourceLayerPanelProps { activeData?: Record; } -export interface DraggedOperation extends DraggingIdentifier { +export interface DragDropOperation { layerId: string; groupId: string; columnId: string; filterOperations: (operation: OperationMetadata) => boolean; } -export function isDraggedOperation( - operationCandidate: unknown -): operationCandidate is DraggedOperation { +export function isOperation(operationCandidate: unknown): operationCandidate is DragDropOperation { return ( typeof operationCandidate === 'object' && operationCandidate !== null && @@ -471,10 +476,8 @@ export function isDraggedOperation( ); } -export type DatasourceDimensionDropProps = SharedDimensionProps & { - layerId: string; - groupId: string; - columnId: string; +export interface DatasourceDimensionDropProps { + target: DragDropOperation; state: T; setState: StateSetter< T, @@ -484,10 +487,10 @@ export type DatasourceDimensionDropProps = SharedDimensionProps & { } >; dimensionGroups: VisualizationDimensionGroupConfig[]; -}; +} -export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & { - droppedItem: unknown; +export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & { + source: DragDropIdentifier; dropType: DropType; }; @@ -851,7 +854,17 @@ export interface Visualization { * look at its internal state to determine which dimension is being affected. */ removeDimension: (props: VisualizationDimensionChangeProps) => T; - + /** + * Allow defining custom behavior for the visualization when the drop action occurs. + */ + onDrop?: (props: { + prevState: T; + target: DragDropOperation; + source: DragDropIdentifier; + frame: FramePublicAPI; + dropType: DropType; + group?: VisualizationDimensionGroupConfig; + }) => T; /** * Update the configuration for the visualization. This is used to update the state */ diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx index 35a40623b72aa..692d0f02725b2 100644 --- a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx @@ -118,6 +118,207 @@ export const getAnnotationsSupportedLayer = ( }; }; +const getDefaultAnnotationConfig = (id: string, timestamp: string): EventAnnotationConfig => ({ + label: defaultAnnotationLabel, + key: { + type: 'point_in_time', + timestamp, + }, + icon: 'triangle', + id, +}); + +const createCopiedAnnotation = ( + newId: string, + timestamp: string, + source?: EventAnnotationConfig +): EventAnnotationConfig => { + if (!source) { + return getDefaultAnnotationConfig(newId, timestamp); + } + return { + ...source, + id: newId, + }; +}; + +export const onAnnotationDrop: Visualization['onDrop'] = ({ + prevState, + frame, + source, + target, + dropType, +}) => { + const targetLayer = prevState.layers.find((l) => l.layerId === target.layerId); + const sourceLayer = prevState.layers.find((l) => l.layerId === source.layerId); + if ( + !targetLayer || + !isAnnotationsLayer(targetLayer) || + !sourceLayer || + !isAnnotationsLayer(sourceLayer) + ) { + return prevState; + } + const targetAnnotation = targetLayer.annotations.find(({ id }) => id === target.columnId); + const sourceAnnotation = sourceLayer.annotations.find(({ id }) => id === source.columnId); + switch (dropType) { + case 'reorder': + if (!targetAnnotation || !sourceAnnotation || source.layerId !== target.layerId) { + return prevState; + } + const newAnnotations = targetLayer.annotations.filter((c) => c.id !== sourceAnnotation.id); + const targetPosition = newAnnotations.findIndex((c) => c.id === targetAnnotation.id); + const targetIndex = targetLayer.annotations.indexOf(sourceAnnotation); + const sourceIndex = targetLayer.annotations.indexOf(targetAnnotation); + newAnnotations.splice( + targetIndex < sourceIndex ? targetPosition + 1 : targetPosition, + 0, + sourceAnnotation + ); + return { + ...prevState, + layers: prevState.layers.map((l) => + l.layerId === target.layerId ? { ...targetLayer, annotations: newAnnotations } : l + ), + }; + case 'swap_compatible': + if (!targetAnnotation || !sourceAnnotation) { + return prevState; + } + return { + ...prevState, + layers: prevState.layers.map((l): XYLayerConfig => { + if (!isAnnotationsLayer(l) || !isAnnotationsLayer(targetLayer)) { + return l; + } + if (l.layerId === target.layerId) { + return { + ...targetLayer, + annotations: [ + ...targetLayer.annotations.map( + (a): EventAnnotationConfig => (a === targetAnnotation ? sourceAnnotation : a) + ), + ], + }; + } + if (l.layerId === source.layerId) { + return { + ...sourceLayer, + annotations: [ + ...sourceLayer.annotations.map( + (a): EventAnnotationConfig => (a === sourceAnnotation ? targetAnnotation : a) + ), + ], + }; + } + return l; + }), + }; + case 'replace_compatible': + if (!targetAnnotation || !sourceAnnotation) { + return prevState; + } + + return { + ...prevState, + layers: prevState.layers.map((l) => { + if (l.layerId === source.layerId) { + return { + ...sourceLayer, + annotations: sourceLayer.annotations.filter((a) => a !== sourceAnnotation), + }; + } + if (l.layerId === target.layerId) { + return { + ...targetLayer, + annotations: [ + ...targetLayer.annotations.map((a) => + a === targetAnnotation ? sourceAnnotation : a + ), + ], + }; + } + return l; + }), + }; + case 'duplicate_compatible': + if (targetAnnotation) { + return prevState; + } + return { + ...prevState, + layers: prevState.layers.map( + (l): XYLayerConfig => + l.layerId === target.layerId + ? { + ...targetLayer, + annotations: [ + ...targetLayer.annotations, + createCopiedAnnotation( + target.columnId, + getStaticDate(getDataLayers(prevState.layers), frame), + sourceAnnotation + ), + ], + } + : l + ), + }; + case 'replace_duplicate_compatible': + if (!targetAnnotation) { + return prevState; + } + return { + ...prevState, + layers: prevState.layers.map((l) => { + if (l.layerId === target.layerId) { + return { + ...targetLayer, + annotations: [ + ...targetLayer.annotations.map((a) => + a === targetAnnotation + ? createCopiedAnnotation( + target.columnId, + getStaticDate(getDataLayers(prevState.layers), frame), + sourceAnnotation + ) + : a + ), + ], + }; + } + return l; + }), + }; + case 'move_compatible': + if (targetAnnotation || !sourceAnnotation) { + return prevState; + } + + return { + ...prevState, + layers: prevState.layers.map((l): XYLayerConfig => { + if (l.layerId === source.layerId) { + return { + ...sourceLayer, + annotations: sourceLayer.annotations.filter((a) => a !== sourceAnnotation), + }; + } + if (l.layerId === target.layerId) { + return { + ...targetLayer, + annotations: [...targetLayer.annotations, sourceAnnotation], + }; + } + return l; + }), + }; + default: + return prevState; + } + return prevState; +}; + export const setAnnotationsDimension: Visualization['setDimension'] = ({ prevState, layerId, @@ -125,46 +326,30 @@ export const setAnnotationsDimension: Visualization['setDimension'] = ( previousColumn, frame, }) => { - const foundLayer = prevState.layers.find((l) => l.layerId === layerId); - if (!foundLayer || !isAnnotationsLayer(foundLayer)) { + const targetLayer = prevState.layers.find((l) => l.layerId === layerId); + if (!targetLayer || !isAnnotationsLayer(targetLayer)) { return prevState; } - const inputAnnotations = foundLayer.annotations as XYAnnotationLayerConfig['annotations']; - const currentConfig = inputAnnotations?.find(({ id }) => id === columnId); - const previousConfig = previousColumn - ? inputAnnotations?.find(({ id }) => id === previousColumn) + const sourceAnnotation = previousColumn + ? targetLayer.annotations?.find(({ id }) => id === previousColumn) : undefined; - let resultAnnotations = [...inputAnnotations] as XYAnnotationLayerConfig['annotations']; - - if (!currentConfig) { - resultAnnotations.push({ - label: defaultAnnotationLabel, - key: { - type: 'point_in_time', - timestamp: getStaticDate(getDataLayers(prevState.layers), frame), - }, - icon: 'triangle', - ...previousConfig, - id: columnId, - }); - } else if (currentConfig && previousConfig) { - // TODO: reordering should not live in setDimension, to be refactored - resultAnnotations = inputAnnotations.filter((c) => c.id !== previousConfig.id); - const targetPosition = resultAnnotations.findIndex((c) => c.id === currentConfig.id); - const targetIndex = inputAnnotations.indexOf(previousConfig); - const sourceIndex = inputAnnotations.indexOf(currentConfig); - resultAnnotations.splice( - targetIndex < sourceIndex ? targetPosition + 1 : targetPosition, - 0, - previousConfig - ); - } - return { ...prevState, layers: prevState.layers.map((l) => - l.layerId === layerId ? { ...foundLayer, annotations: resultAnnotations } : l + l.layerId === layerId + ? { + ...targetLayer, + annotations: [ + ...targetLayer.annotations, + createCopiedAnnotation( + columnId, + getStaticDate(getDataLayers(prevState.layers), frame), + sourceAnnotation + ), + ], + } + : l ), }; }; @@ -224,7 +409,6 @@ export const getAnnotationsConfiguration = ({ defaultMessage: 'Annotations require a time based chart to work. Add a date histogram.', }), required: false, - requiresPreviousColumnOnDuplicate: true, supportsMoreColumns: true, supportFieldFormat: false, enableDimensionEditor: true, diff --git a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx index 3f0e8816cf6b1..ece9a6d28893e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx @@ -18,6 +18,7 @@ import { checkScaleOperation, getAxisName, getDataLayers, + getReferenceLayers, isNumericMetric, isReferenceLayer, } from './visualization_helpers'; @@ -342,7 +343,10 @@ export const setReferenceDimension: Visualization['setDimension'] = ({ newLayer.accessors = [...newLayer.accessors.filter((a) => a !== columnId), columnId]; const hasYConfig = newLayer.yConfig?.some(({ forAccessor }) => forAccessor === columnId); const previousYConfig = previousColumn - ? newLayer.yConfig?.find(({ forAccessor }) => forAccessor === previousColumn) + ? getReferenceLayers(prevState.layers) + .map(({ yConfig }) => yConfig) + .flat() + ?.find((yConfig) => yConfig?.forAccessor === previousColumn) : false; if (!hasYConfig) { const axisMode: YAxisMode = diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index 1cc3df6b5ca96..0092bb78a6d71 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -476,7 +476,7 @@ describe('xy_visualization', () => { }); it('should copy previous column if passed and assign a new id', () => { expect( - xyVisualization.setDimension({ + xyVisualization.onDrop!({ frame, prevState: { ...exampleState(), @@ -488,10 +488,20 @@ describe('xy_visualization', () => { }, ], }, - layerId: 'annotation', - groupId: 'xAnnotation', - previousColumn: 'an2', - columnId: 'newColId', + dropType: 'duplicate_compatible', + source: { + layerId: 'annotation', + groupId: 'xAnnotation', + columnId: 'an2', + id: 'an2', + humanData: { label: 'an2' }, + }, + target: { + layerId: 'annotation', + groupId: 'xAnnotation', + columnId: 'newColId', + filterOperations: Boolean, + }, }).layers[0] ).toEqual({ layerId: 'annotation', @@ -501,7 +511,7 @@ describe('xy_visualization', () => { }); it('should reorder a dimension to a annotation layer', () => { expect( - xyVisualization.setDimension({ + xyVisualization.onDrop!({ frame, prevState: { ...exampleState(), @@ -513,10 +523,21 @@ describe('xy_visualization', () => { }, ], }, - layerId: 'annotation', - groupId: 'xAnnotation', - previousColumn: 'an2', - columnId: 'an1', + source: { + layerId: 'annotation', + groupId: 'xAnnotation', + columnId: 'an2', + id: 'an2', + humanData: { label: 'label' }, + filterOperations: () => true, + }, + target: { + layerId: 'annotation', + groupId: 'xAnnotation', + columnId: 'an1', + filterOperations: () => true, + }, + dropType: 'reorder', }).layers[0] ).toEqual({ layerId: 'annotation', @@ -524,6 +545,199 @@ describe('xy_visualization', () => { annotations: [exampleAnnotation2, exampleAnnotation], }); }); + + it('should duplicate the annotations and replace the target in another annotation layer', () => { + expect( + xyVisualization.onDrop!({ + frame, + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + layerType: 'annotations', + annotations: [exampleAnnotation], + }, + { + layerId: 'second', + layerType: 'annotations', + annotations: [exampleAnnotation2], + }, + ], + }, + source: { + layerId: 'first', + groupId: 'xAnnotation', + columnId: 'an1', + id: 'an1', + humanData: { label: 'label' }, + filterOperations: () => true, + }, + target: { + layerId: 'second', + groupId: 'xAnnotation', + columnId: 'an2', + filterOperations: () => true, + }, + dropType: 'replace_duplicate_compatible', + }).layers + ).toEqual([ + { + layerId: 'first', + layerType: layerTypes.ANNOTATIONS, + annotations: [exampleAnnotation], + }, + { + layerId: 'second', + layerType: layerTypes.ANNOTATIONS, + annotations: [{ ...exampleAnnotation, id: 'an2' }], + }, + ]); + }); + it('should swap the annotations between layers', () => { + expect( + xyVisualization.onDrop!({ + frame, + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + layerType: 'annotations', + annotations: [exampleAnnotation], + }, + { + layerId: 'second', + layerType: 'annotations', + annotations: [exampleAnnotation2], + }, + ], + }, + source: { + layerId: 'first', + groupId: 'xAnnotation', + columnId: 'an1', + id: 'an1', + humanData: { label: 'label' }, + filterOperations: () => true, + }, + target: { + layerId: 'second', + groupId: 'xAnnotation', + columnId: 'an2', + filterOperations: () => true, + }, + dropType: 'swap_compatible', + }).layers + ).toEqual([ + { + layerId: 'first', + layerType: layerTypes.ANNOTATIONS, + annotations: [exampleAnnotation2], + }, + { + layerId: 'second', + layerType: layerTypes.ANNOTATIONS, + annotations: [exampleAnnotation], + }, + ]); + }); + it('should replace the target in another annotation layer', () => { + expect( + xyVisualization.onDrop!({ + frame, + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + layerType: 'annotations', + annotations: [exampleAnnotation], + }, + { + layerId: 'second', + layerType: 'annotations', + annotations: [exampleAnnotation2], + }, + ], + }, + source: { + layerId: 'first', + groupId: 'xAnnotation', + columnId: 'an1', + id: 'an1', + humanData: { label: 'label' }, + filterOperations: () => true, + }, + target: { + layerId: 'second', + groupId: 'xAnnotation', + columnId: 'an2', + filterOperations: () => true, + }, + dropType: 'replace_compatible', + }).layers + ).toEqual([ + { + layerId: 'first', + layerType: layerTypes.ANNOTATIONS, + annotations: [], + }, + { + layerId: 'second', + layerType: layerTypes.ANNOTATIONS, + annotations: [exampleAnnotation], + }, + ]); + }); + it('should move compatible to another annotation layer', () => { + expect( + xyVisualization.onDrop!({ + frame, + prevState: { + ...exampleState(), + layers: [ + { + layerId: 'first', + layerType: 'annotations', + annotations: [exampleAnnotation], + }, + { + layerId: 'second', + layerType: 'annotations', + annotations: [], + }, + ], + }, + source: { + layerId: 'first', + groupId: 'xAnnotation', + columnId: 'an1', + id: 'an1', + humanData: { label: 'label' }, + filterOperations: () => true, + }, + target: { + layerId: 'second', + groupId: 'xAnnotation', + columnId: 'an2', + filterOperations: () => true, + }, + dropType: 'move_compatible', + }).layers + ).toEqual([ + { + layerId: 'first', + layerType: layerTypes.ANNOTATIONS, + annotations: [], + }, + { + layerId: 'second', + layerType: layerTypes.ANNOTATIONS, + annotations: [exampleAnnotation], + }, + ]); + }); }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 4f89ba1fdcedf..dcf3ab3e42a47 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -27,7 +27,7 @@ import { getSuggestions } from './xy_suggestions'; import { XyToolbar } from './xy_config_panel'; import { DimensionEditor } from './xy_config_panel/dimension_editor'; import { LayerHeader } from './xy_config_panel/layer_header'; -import type { Visualization, AccessorConfig, FramePublicAPI } from '../types'; +import { Visualization, AccessorConfig, FramePublicAPI } from '../types'; import { State, visualizationTypes, XYSuggestion, XYLayerConfig, XYDataLayerConfig } from './types'; import { layerTypes } from '../../common'; import { isHorizontalChart } from './state_helpers'; @@ -45,6 +45,7 @@ import { getAnnotationsSupportedLayer, setAnnotationsDimension, getUniqueLabels, + onAnnotationDrop, } from './annotations/helpers'; import { checkXAccessorCompatibility, @@ -71,6 +72,7 @@ import { ReferenceLinePanel } from './xy_config_panel/reference_line_config_pane import { AnnotationsPanel } from './xy_config_panel/annotations_config_panel'; import { DimensionTrigger } from '../shared_components/dimension_trigger'; import { defaultAnnotationLabel } from './annotations/helpers'; +import { onDropForVisualization } from '../editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils'; export const getXyVisualization = ({ datatableUtilities, @@ -303,6 +305,20 @@ export const getXyVisualization = ({ return getFirstDataLayer(state.layers)?.palette; }, + onDrop(props) { + const targetLayer: XYLayerConfig | undefined = props.prevState.layers.find( + (l) => l.layerId === props.target.layerId + ); + if (!targetLayer) { + throw new Error('target layer should exist'); + } + + if (isAnnotationsLayer(targetLayer)) { + return onAnnotationDrop?.(props) || props.prevState; + } + return onDropForVisualization(props, this); + }, + setDimension(props) { const { prevState, layerId, columnId, groupId } = props; diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx index d390d081258a5..9b4ed872f1dc2 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx @@ -334,6 +334,7 @@ export function validateLayersForDimension( export const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; + export const isNumericDynamicMetric = (op: OperationMetadata) => isNumericMetric(op) && !op.isStaticValue; export const isBucketed = (op: OperationMetadata) => op.isBucketed; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 3eeda946ada8f..ae829d05815fb 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -209,48 +209,14 @@ "xpack.lens.dragDrop.announce.cancelled": "Mouvement annulé. {label} revenu à sa position initiale", "xpack.lens.dragDrop.announce.cancelledItem": "Mouvement annulé. {label} revenu au groupe {groupLabel} à la position {position}", "xpack.lens.dragDrop.announce.combine.short": " Maintenir la touche Contrôle enfoncée pour combiner", - "xpack.lens.dragDrop.announce.dropped.combineCompatible": "Combinaison de {label} avec {dropGroupLabel} à la position {dropPosition} et de {dropLabel} avec {groupLabel} à la position {position}", - "xpack.lens.dragDrop.announce.dropped.combineIncompatible": "Conversion de {label} en {nextLabel} dans le groupe {groupLabel} à la position {position} et combinaison avec {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition}", - "xpack.lens.dragDrop.announce.dropped.duplicated": "{label} dupliqué dans le groupe {groupLabel} à la position {position}", - "xpack.lens.dragDrop.announce.dropped.duplicateIncompatible": "Copie de {label} convertie en {nextLabel} et ajoutée au groupe {groupLabel} à la position {position}", - "xpack.lens.dragDrop.announce.dropped.moveCompatible": "{label} déplacé dans le groupe {groupLabel} à la position {position}", - "xpack.lens.dragDrop.announce.dropped.moveIncompatible": "{label} converti en {nextLabel} et déplacé dans le groupe {groupLabel} à la position {position}", "xpack.lens.dragDrop.announce.dropped.reordered": "{label} réorganisé dans le groupe {groupLabel} de la position {prevPosition} à la position {position}", - "xpack.lens.dragDrop.announce.dropped.replaceDuplicateIncompatible": "Copie de {label} convertie en {nextLabel} et {dropLabel} remplacé dans le groupe {groupLabel} à la position {position}", - "xpack.lens.dragDrop.announce.dropped.replaceIncompatible": "{label} converti en {nextLabel} et {dropLabel} remplacé dans le groupe {groupLabel} à la position {position}", - "xpack.lens.dragDrop.announce.dropped.swapCompatible": "{label} déplacé dans {dropGroupLabel} à la position {dropPosition} et {dropLabel} dans {groupLabel} à la position {position}", - "xpack.lens.dragDrop.announce.dropped.swapIncompatible": "{label} converti en {nextLabel} dans le groupe {groupLabel} à la position {position} et permuté avec {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition}", - "xpack.lens.dragDrop.announce.droppedDefault": "{label} ajouté dans le groupe {dropGroupLabel} à la position {position}", "xpack.lens.dragDrop.announce.droppedNoPosition": "{label} ajouté à {dropLabel}", "xpack.lens.dragDrop.announce.duplicate.short": " Maintenez la touche Alt ou Option enfoncée pour dupliquer.", - "xpack.lens.dragDrop.announce.duplicated.combine": "Combiner {dropLabel} avec {label} dans {groupLabel} à la position {position}", - "xpack.lens.dragDrop.announce.duplicated.replace": "{dropLabel} remplacé par {label} dans {groupLabel} à la position {position}", - "xpack.lens.dragDrop.announce.duplicated.replaceDuplicateCompatible": "{dropLabel} remplacé par une copie de {label} dans {groupLabel} à la position {position}", "xpack.lens.dragDrop.announce.lifted": "{label} levé", - "xpack.lens.dragDrop.announce.selectedTarget.combine": "Combinez {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition} avec {label}. Appuyez sur la barre d’espace ou sur Entrée pour combiner.", - "xpack.lens.dragDrop.announce.selectedTarget.combineCompatible": "Combinez {label} dans le groupe {groupLabel} à la position {position} avec {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition}. Maintenez la touche Contrôle enfoncée et appuyez sur la barre d’espace ou sur Entrée pour combiner.", - "xpack.lens.dragDrop.announce.selectedTarget.combineIncompatible": "Convertissez {label} en {nextLabel} dans le groupe {groupLabel} à la position {position} et combinez avec {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition}. Maintenez la touche Contrôle enfoncée et appuyez sur la barre d’espace ou sur Entrée pour combiner.", - "xpack.lens.dragDrop.announce.selectedTarget.combineMain": "Vous faites glisser {label} à partir de {groupLabel} à la position {position} sur {dropLabel} à partir du groupe {dropGroupLabel} à la position {dropPosition}. Appuyer sur la barre d’espace ou sur Entrée pour combiner {dropLabel} avec {label}.{duplicateCopy}{swapCopy}{combineCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.default": "Ajoutez {label} au groupe {dropGroupLabel} à la position {position}. Appuyer sur la barre d'espace ou sur Entrée pour ajouter", "xpack.lens.dragDrop.announce.selectedTarget.defaultNoPosition": "Ajoutez {label} à {dropLabel}. Appuyer sur la barre d'espace ou sur Entrée pour ajouter", - "xpack.lens.dragDrop.announce.selectedTarget.duplicated": "Dupliquez {label} dans le groupe {dropGroupLabel} à la position {position}. Maintenir la touche Alt ou Option enfoncée et appuyer sur la barre d'espace ou sur Entrée pour dupliquer", - "xpack.lens.dragDrop.announce.selectedTarget.duplicatedInGroup": "Dupliquez {label} dans le groupe {dropGroupLabel} à la position {position}. Appuyer sur la barre d'espace ou sur Entrée pour dupliquer", - "xpack.lens.dragDrop.announce.selectedTarget.duplicateIncompatible": "Convertissez la copie de {label} en {nextLabel} et ajoutez-la au groupe {groupLabel} à la position {position}. Maintenir la touche Alt ou Option enfoncée et appuyer sur la barre d'espace ou sur Entrée pour dupliquer", - "xpack.lens.dragDrop.announce.selectedTarget.moveCompatible": "Déplacez {label} dans le groupe {dropGroupLabel} à la position {dropPosition}. Appuyer sur la barre d'espace ou sur Entrée pour déplacer", - "xpack.lens.dragDrop.announce.selectedTarget.moveCompatibleMain": "Vous faites glisser {label} de {groupLabel} à la position {position} vers la position {dropPosition} dans le groupe {dropGroupLabel}. Appuyez sur la barre d'espace ou sur Entrée pour déplacer.{duplicateCopy}{swapCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.moveIncompatible": "Convertissez {label} en {nextLabel} et déplacez-le dans le groupe {dropGroupLabel} à la position {dropPosition}. Appuyer sur la barre d'espace ou sur Entrée pour déplacer", - "xpack.lens.dragDrop.announce.selectedTarget.moveIncompatibleMain": "Vous faites glisser {label} de {groupLabel} à la position {position} vers la position {dropPosition} dans le groupe {dropGroupLabel}. Appuyez sur la barre d'espace ou sur Entrée pour convertir {label} en {nextLabel} et déplacer.{duplicateCopy}{swapCopy}", "xpack.lens.dragDrop.announce.selectedTarget.noSelected": "Aucune cible sélectionnée. Utiliser les touches fléchées pour sélectionner une cible", "xpack.lens.dragDrop.announce.selectedTarget.reordered": "Réorganisez {label} dans le groupe {groupLabel} de la position {prevPosition} à la position {position}. Appuyer sur la barre d'espace ou sur Entrée pour réorganiser", "xpack.lens.dragDrop.announce.selectedTarget.reorderedBack": "{label} revenu à sa position initiale {prevPosition}", - "xpack.lens.dragDrop.announce.selectedTarget.replace": "Remplacez {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition} avec {label}. Appuyez sur la barre d'espace ou sur Entrée pour remplacer.", - "xpack.lens.dragDrop.announce.selectedTarget.replaceDuplicateCompatible": "Dupliquez {label} et remplacez {dropLabel} dans {groupLabel} à la position {position}. Maintenir la touche Alt ou Option enfoncée et appuyer sur la barre d'espace ou sur Entrée pour dupliquer et remplacer", - "xpack.lens.dragDrop.announce.selectedTarget.replaceDuplicateIncompatible": "Convertissez la copie de {label} en {nextLabel} et remplacez {dropLabel} dans le groupe {groupLabel} à la position {position}. Maintenir la touche Alt ou Option enfoncée et appuyer sur la barre d'espace ou sur Entrée pour dupliquer et remplacer", - "xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatible": "Convertissez {label} en {nextLabel} et remplacez {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition}. Appuyer sur la barre d'espace ou sur Entrée pour remplacer", - "xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatibleMain": "Vous faites glisser {label} à partir de {groupLabel} à la position {position} sur {dropLabel} à partir du groupe {dropGroupLabel} à la position {dropPosition}. Appuyer sur la barre d'espace ou sur Entrée pour convertir {label} en {nextLabel} et remplacer {dropLabel}.{duplicateCopy}{swapCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.replaceMain": "Vous faites glisser {label} à partir de {groupLabel} à la position {position} sur {dropLabel} à partir du groupe {dropGroupLabel} à la position {dropPosition}. Appuyer sur la barre d'espace ou sur Entrée pour remplacer {dropLabel} par {label}.{duplicateCopy}{swapCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.swapCompatible": "Permutez {label} dans le groupe {groupLabel} à la position {position} avec {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition}. Maintenir la touche Maj enfoncée tout en appuyant sur la barre d'espace ou sur Entrée pour permuter", - "xpack.lens.dragDrop.announce.selectedTarget.swapIncompatible": "Convertir {label} en {nextLabel} dans le groupe {groupLabel} à la position {position} et permutez avec {dropLabel} dans le groupe {dropGroupLabel} à la position {dropPosition}. Maintenir la touche Maj enfoncée tout en appuyant sur la barre d'espace ou sur Entrée pour permuter", "xpack.lens.dragDrop.announce.swap.short": " Maintenez la touche Maj enfoncée pour permuter.", "xpack.lens.dragDrop.combine": "Combiner", "xpack.lens.dragDrop.control": "Contrôle", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2fa35f6e723b2..e8e2fc397bf63 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -214,43 +214,14 @@ "xpack.lens.dragDrop.announce.cancelled": "移動がキャンセルされました。{label}は初期位置に戻りました", "xpack.lens.dragDrop.announce.cancelledItem": "移動がキャンセルされました。{label}は位置{position}の{groupLabel}グループに戻りました", "xpack.lens.dragDrop.announce.combine.short": " Ctrlを押しながら結合します", - "xpack.lens.dragDrop.announce.dropped.combineCompatible": "位置{dropPosition}で{label}を{dropGroupLabel}に移動し、位置{position}で{dropLabel}を {groupLabel}グループに結合しました", - "xpack.lens.dragDrop.announce.dropped.combineIncompatible": "位置{position}で{label}を{groupLabel}の{nextLabel}に変換し、位置{dropPosition}で{dropGroupLabel}グループの{dropLabel}と結合しました", - "xpack.lens.dragDrop.announce.dropped.duplicated": "位置{position}の{groupLabel}グループで{label}を複製しました", - "xpack.lens.dragDrop.announce.dropped.duplicateIncompatible": "{label}のコピーを{nextLabel}に変換し、位置{position}の{groupLabel}グループに追加しました", - "xpack.lens.dragDrop.announce.dropped.moveCompatible": "位置{position}の{groupLabel}グループに{label}を移動しました", - "xpack.lens.dragDrop.announce.dropped.moveIncompatible": "{label}を{nextLabel}に変換し、位置{position}の{groupLabel}グループに移動しました", "xpack.lens.dragDrop.announce.dropped.reordered": "{groupLabel}グループの{label}を位置{prevPosition}から位置{position}に並べ替えました", - "xpack.lens.dragDrop.announce.dropped.replaceDuplicateIncompatible": "{label}のコピーを{nextLabel}に変換し、位置{position}の{groupLabel}グループで{dropLabel}を置き換えました", - "xpack.lens.dragDrop.announce.dropped.replaceIncompatible": "{label}を{nextLabel}に変換し、位置{position}の{groupLabel}グループで{dropLabel}を置き換えました", - "xpack.lens.dragDrop.announce.dropped.swapCompatible": "位置{dropPosition}で{label}を{dropGroupLabel}に移動し、位置{position}で{dropLabel}を {groupLabel}グループに移動しました", - "xpack.lens.dragDrop.announce.dropped.swapIncompatible": "位置{position}で{label}を{groupLabel}の{nextLabel}に変換し、位置{dropPosition}で{dropGroupLabel}グループの{dropLabel}と入れ替えました", - "xpack.lens.dragDrop.announce.droppedDefault": "位置{position}の{dropGroupLabel}グループで{label}を追加しました", "xpack.lens.dragDrop.announce.droppedNoPosition": "{label}を{dropLabel}に追加しました", "xpack.lens.dragDrop.announce.duplicate.short": " AltキーまたはOptionを押し続けると複製します。", - "xpack.lens.dragDrop.announce.duplicated.combine": "位置{position}の{groupLabel}で{dropLabel}を{label}と結合しました", - "xpack.lens.dragDrop.announce.duplicated.replace": "位置{position}の{groupLabel}で{dropLabel}を{label}に置き換えました", - "xpack.lens.dragDrop.announce.duplicated.replaceDuplicateCompatible": "位置{position}の{groupLabel}で{dropLabel}を{label}のコピーに置き換えました", "xpack.lens.dragDrop.announce.lifted": "{label}を上げました", - "xpack.lens.dragDrop.announce.selectedTarget.combineCompatible": "位置{position}の{groupLabel}の{label}を、位置{dropPosition}の{dropGroupLabel}グループの{dropLabel}と結合します。Ctrlキーを押しながらスペースバーまたはEnterキーを押すと、結合します", - "xpack.lens.dragDrop.announce.selectedTarget.combineIncompatible": "位置{position}で{label}を{groupLabel}の{nextLabel}に変換し、位置{dropPosition}で{dropGroupLabel}グループの{dropLabel}と結合します。Ctrlキーを押しながらスペースバーまたはEnterキーを押すと、結合します", - "xpack.lens.dragDrop.announce.selectedTarget.default": "位置{position}の{dropGroupLabel}グループに{label}を追加しました。スペースまたはEnterを押して追加します", "xpack.lens.dragDrop.announce.selectedTarget.defaultNoPosition": "{label}を{dropLabel}に追加します。スペースまたはEnterを押して追加します", - "xpack.lens.dragDrop.announce.selectedTarget.duplicated": "位置{position}の{dropGroupLabel}グループに{label}を複製しました。AltまたはOptionを押しながらスペースバーまたはEnterキーを押すと、複製します", - "xpack.lens.dragDrop.announce.selectedTarget.duplicatedInGroup": "位置{position}の{dropGroupLabel}グループに{label}を複製しました。スペースまたはEnterを押して複製します", - "xpack.lens.dragDrop.announce.selectedTarget.duplicateIncompatible": "{label}のコピーを{nextLabel}に変換し、位置{position}で{groupLabel}グループに移動します。AltまたはOptionを押しながらスペースバーまたはEnterキーを押すと、複製します", - "xpack.lens.dragDrop.announce.selectedTarget.moveCompatibleMain": "位置{position}で{groupLabel}の{label}を{dropGroupLabel}グループの位置{dropPosition}にドラッグしています。スペースバーまたはEnterキーを押すと移動します。{duplicateCopy}{swapCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.moveIncompatible": "{label}を{nextLabel}に変換し、位置{dropPosition}で{dropGroupLabel}グループに移動します。スペースまたはEnterを押して移動します", - "xpack.lens.dragDrop.announce.selectedTarget.moveIncompatibleMain": "位置{position}で{groupLabel}の{label}を{dropGroupLabel}グループの位置{dropPosition}にドラッグしています。スペースバーまたはEnterキーを押して、{label}を{nextLabel}に変換して移動します。{duplicateCopy}{swapCopy}", "xpack.lens.dragDrop.announce.selectedTarget.noSelected": "対象が選択されていません。矢印キーを使用して対象を選択してください", "xpack.lens.dragDrop.announce.selectedTarget.reordered": "{groupLabel}グループの{label}を位置{prevPosition}から位置{position}に並べ替えます。スペースまたはEnterを押して並べ替えます", "xpack.lens.dragDrop.announce.selectedTarget.reorderedBack": "{label}は初期位置{prevPosition}に戻りました", - "xpack.lens.dragDrop.announce.selectedTarget.replaceDuplicateCompatible": "位置{position}で{label}を複製し、{groupLabel}グループで{dropLabel}を置き換えます。AltまたはOptionを押しながらスペースバーまたはEnterキーを押すと、複製して置換します", - "xpack.lens.dragDrop.announce.selectedTarget.replaceDuplicateIncompatible": "{label}のコピーを{nextLabel}に変換し、位置{position}で{groupLabel}グループの{dropLabel}を置き換えます。AltまたはOptionを押しながらスペースバーまたはEnterキーを押すと、複製して置換します", - "xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatibleMain": "位置{position}の{groupLabel}の{label}を、位置{dropPosition}の{dropGroupLabel}グループの{dropLabel}にドラッグしています。スペースバーまたはEnterキーを押して、{label}を{nextLabel}に変換して、{dropLabel}を置き換えます。{duplicateCopy}{swapCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.replaceMain": "位置{position}の{groupLabel}の{label}を、位置{dropPosition}の{dropGroupLabel}グループの{dropLabel}にドラッグしています。スペースまたはEnterを押して、{dropLabel}を{label}で置き換えます。{duplicateCopy}{swapCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.swapCompatible": "位置{position}の{groupLabel}の{label}を、位置{dropPosition}の{dropGroupLabel}グループの{dropLabel}と入れ替えます。Shiftキーを押しながらスペースバーまたはEnterキーを押すと、入れ替えます", - "xpack.lens.dragDrop.announce.selectedTarget.swapIncompatible": "位置{position}で{label}を{groupLabel}の{nextLabel}に変換し、位置{dropPosition}で{dropGroupLabel}グループの{dropLabel}と入れ替えます。Shiftキーを押しながらスペースバーまたはEnterキーを押すと、入れ替えます", "xpack.lens.dragDrop.announce.swap.short": " Shiftキーを押すと入れ替えます。", "xpack.lens.dragDrop.combine": "結合", "xpack.lens.dragDrop.control": "Control", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 01b30dda57b2c..09d512eae30db 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -214,48 +214,14 @@ "xpack.lens.dragDrop.announce.cancelled": "移动已取消。{label} 将返回至其初始位置", "xpack.lens.dragDrop.announce.cancelledItem": "移动已取消。{label} 返回至 {groupLabel} 组中的位置 {position}", "xpack.lens.dragDrop.announce.combine.short": " 按住 Control 键组合", - "xpack.lens.dragDrop.announce.dropped.combineCompatible": "已将 {label} 组合到 {dropGroupLabel} 中的位置 {dropPosition} 并将 {dropLabel} 组合到组 {groupLabel} 中的位置 {position}", - "xpack.lens.dragDrop.announce.dropped.combineIncompatible": "已将 {label} 转换为组 {groupLabel} 中位置 {position} 上的 {nextLabel},并与组 {dropGroupLabel} 中位置 {dropPosition} 上的 {dropLabel} 组合", - "xpack.lens.dragDrop.announce.dropped.duplicated": "已在 {groupLabel} 组中的位置 {position} 复制 {label}", - "xpack.lens.dragDrop.announce.dropped.duplicateIncompatible": "已将 {label} 的副本转换为 {nextLabel} 并添加 {groupLabel} 组中的位置 {position}", - "xpack.lens.dragDrop.announce.dropped.moveCompatible": "已将 {label} 移到 {groupLabel} 组中的位置 {position}", - "xpack.lens.dragDrop.announce.dropped.moveIncompatible": "已将 {label} 转换为 {nextLabel} 并移到 {groupLabel} 组中的位置 {position}", "xpack.lens.dragDrop.announce.dropped.reordered": "已将 {groupLabel} 组中的 {label} 从位置 {prevPosition} 重新排到位置 {position}", - "xpack.lens.dragDrop.announce.dropped.replaceDuplicateIncompatible": "已将 {label} 的副本转换为 {nextLabel} 并替换了 {groupLabel} 组中位置 {position} 上的 {dropLabel}", - "xpack.lens.dragDrop.announce.dropped.replaceIncompatible": "已将 {label} 转换为 {nextLabel} 并替换了 {groupLabel} 组中位置 {position} 上的 {dropLabel}", - "xpack.lens.dragDrop.announce.dropped.swapCompatible": "已将 {label} 移至 {dropGroupLabel} 中的位置 {dropPosition} 并将 {dropLabel} 移至组 {groupLabel} 中的位置 {position}", - "xpack.lens.dragDrop.announce.dropped.swapIncompatible": "已将 {label} 转换为组 {groupLabel} 中位置 {position} 上的 {nextLabel},并与组 {dropGroupLabel} 中位置 {dropPosition} 上的 {dropLabel} 交换", - "xpack.lens.dragDrop.announce.droppedDefault": "已将 {label} 添加到 {dropGroupLabel} 组中的位置 {position}", "xpack.lens.dragDrop.announce.droppedNoPosition": "已将 {label} 添加到 {dropLabel}", "xpack.lens.dragDrop.announce.duplicate.short": " 按住 alt 或 option 键以复制。", - "xpack.lens.dragDrop.announce.duplicated.combine": "将 {dropLabel} 与 {groupLabel} 中位置 {position} 上的 {label} 组合", - "xpack.lens.dragDrop.announce.duplicated.replace": "已将 {groupLabel} 组中位置 {position} 上的 {dropLabel} 替换为 {label}", - "xpack.lens.dragDrop.announce.duplicated.replaceDuplicateCompatible": "已将 {groupLabel} 组中位置 {position} 上的 {dropLabel} 替换为 {label} 的副本", "xpack.lens.dragDrop.announce.lifted": "已提升 {label}", - "xpack.lens.dragDrop.announce.selectedTarget.combine": "将 {dropGroupLabel} 组中位置 {dropPosition} 上的 {dropLabel} 与 {label} 组合。按空格键或 enter 键组合。", - "xpack.lens.dragDrop.announce.selectedTarget.combineCompatible": "将组 {groupLabel} 中位置 {position} 上的 {label} 与组 {dropGroupLabel} 中位置 {dropPosition} 上的 {dropLabel} 组合。按住 Control 键并按空格键或 enter 键组合", - "xpack.lens.dragDrop.announce.selectedTarget.combineIncompatible": "将 {label} 转换为组 {groupLabel} 中位置 {position} 上的 {nextLabel},并与组 {dropGroupLabel} 中位置 {dropPosition} 上的 {dropLabel} 组合。按住 Control 键并按空格键或 enter 键组合", - "xpack.lens.dragDrop.announce.selectedTarget.combineMain": "您正将 {groupLabel} 中位置 {position} 上的{label} 拖到 {dropGroupLabel} 组中位置 {dropPosition} 上的 {dropLabel}。按空格键或 enter 键以将 {dropLabel} 与 {label} 组合。{duplicateCopy}{swapCopy}{combineCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.default": "将 {label} 添加到 {dropGroupLabel} 组中的位置 {position}。按空格键或 enter 键添加", "xpack.lens.dragDrop.announce.selectedTarget.defaultNoPosition": "将 {label} 添加到 {dropLabel}。按空格键或 enter 键添加", - "xpack.lens.dragDrop.announce.selectedTarget.duplicated": "将 {label} 复制到 {dropGroupLabel} 组中的位置 {position}。按住 Alt 或 Option 并按空格键或 enter 键以复制", - "xpack.lens.dragDrop.announce.selectedTarget.duplicatedInGroup": "将 {label} 复制到 {dropGroupLabel} 组中的位置 {position}。按空格键或 enter 键复制", - "xpack.lens.dragDrop.announce.selectedTarget.duplicateIncompatible": "将 {label} 转换为 {nextLabel} 并移到 {groupLabel} 组中的位置 {position}。按住 Alt 或 Option 并按空格键或 enter 键以复制", - "xpack.lens.dragDrop.announce.selectedTarget.moveCompatible": "将 {label} 移至 {dropGroupLabel} 组中的位置 {dropPosition}。按空格键或 enter 键移动", - "xpack.lens.dragDrop.announce.selectedTarget.moveCompatibleMain": "您正将 {groupLabel} 中位置 {position} 上的{label} 拖到 {dropGroupLabel} 组中的位置 {dropPosition} 上。按空格键或 enter 键移动。{duplicateCopy}{swapCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.moveIncompatible": "将 {label} 转换为 {nextLabel} 并移到 {dropGroupLabel} 组中的位置 {dropPosition}。按空格键或 enter 键移动", - "xpack.lens.dragDrop.announce.selectedTarget.moveIncompatibleMain": "您正将 {groupLabel} 中位置 {position} 上的{label} 拖到 {dropGroupLabel} 组中的位置 {dropPosition} 上。按空格键或 enter 键以将 {label} 转换为 {nextLabel} 并移动。{duplicateCopy}{swapCopy}", "xpack.lens.dragDrop.announce.selectedTarget.noSelected": "未选择任何目标。使用箭头键选择目标", "xpack.lens.dragDrop.announce.selectedTarget.reordered": "将 {groupLabel} 组中的 {label} 从位置 {prevPosition} 重新排到位置 {position}。按空格键或 enter 键重新排列", "xpack.lens.dragDrop.announce.selectedTarget.reorderedBack": "{label} 已返回至其初始位置 {prevPosition}", - "xpack.lens.dragDrop.announce.selectedTarget.replace": "将 {dropGroupLabel} 组中位置 {dropPosition} 上的 {dropLabel} 替换为 {label}。按空格键或 enter 键替换。", - "xpack.lens.dragDrop.announce.selectedTarget.replaceDuplicateCompatible": "复制 {label} 并替换 {groupLabel} 中位置 {position} 上的 {dropLabel}。按住 Alt 或 Option 并按空格键或 enter 键以复制并替换", - "xpack.lens.dragDrop.announce.selectedTarget.replaceDuplicateIncompatible": "将 {label} 的副本转换为 {nextLabel} 并替换 {groupLabel} 组中位置 {position} 上的 {dropLabel}。按住 Alt 或 Option 并按空格键或 enter 键以复制并替换", - "xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatible": "将 {label} 转换为 {nextLabel} 并替换 {dropGroupLabel} 组中位置 {dropPosition} 上的 {dropLabel}。按空格键或 enter 键替换", - "xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatibleMain": "您正将 {groupLabel} 中位置 {position} 上的{label} 拖到 {dropGroupLabel} 组中位置 {dropPosition} 上的 {dropLabel}。按空格键或 enter 键以将 {label} 转换为 {nextLabel} 并替换 {dropLabel}。{duplicateCopy}{swapCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.replaceMain": "您正将 {groupLabel} 中位置 {position} 上的{label} 拖到 {dropGroupLabel} 组中位置 {dropPosition} 上的 {dropLabel}。按空格键或 enter 键以将 {dropLabel} 替换为 {label}。{duplicateCopy}{swapCopy}", - "xpack.lens.dragDrop.announce.selectedTarget.swapCompatible": "将组 {groupLabel} 中位置 {position} 上的 {label} 与组 {dropGroupLabel} 中位置 {dropPosition} 上的 {dropLabel} 交换。按住 Shift 键并按空格键或 enter 键交换", - "xpack.lens.dragDrop.announce.selectedTarget.swapIncompatible": "将 {label} 转换为组 {groupLabel} 中位置 {position} 上的 {nextLabel},并与组 {dropGroupLabel} 中位置 {dropPosition} 上的 {dropLabel} 交换。按住 Shift 键并按空格键或 enter 键交换", "xpack.lens.dragDrop.announce.swap.short": " 按住 Shift 键交换。", "xpack.lens.dragDrop.combine": "组合", "xpack.lens.dragDrop.control": "Control 键", diff --git a/x-pack/test/accessibility/apps/lens.ts b/x-pack/test/accessibility/apps/lens.ts index 18459b56c0542..854ff1b349e49 100644 --- a/x-pack/test/accessibility/apps/lens.ts +++ b/x-pack/test/accessibility/apps/lens.ts @@ -146,23 +146,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.createLayer(); await PageObjects.lens.switchToVisualization('area'); - await PageObjects.lens.configureDimension( - { - dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', - operation: 'date_histogram', - field: '@timestamp', - }, - 1 - ); + await PageObjects.lens.configureDimension({ + dimension: 'lns-layerPanel-1 > lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); - await PageObjects.lens.configureDimension( - { - dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', - operation: 'median', - field: 'bytes', - }, - 1 - ); + await PageObjects.lens.configureDimension({ + dimension: 'lns-layerPanel-1 > lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'median', + field: 'bytes', + }); await a11y.testAppSnapshot(); }); diff --git a/x-pack/test/functional/apps/lens/group1/smokescreen.ts b/x-pack/test/functional/apps/lens/group1/smokescreen.ts index 70887b337114f..2f82218b42a7a 100644 --- a/x-pack/test/functional/apps/lens/group1/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/group1/smokescreen.ts @@ -125,23 +125,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await PageObjects.lens.hasChartSwitchWarning('line')).to.eql(false); await PageObjects.lens.switchToVisualization('line'); - await PageObjects.lens.configureDimension( - { - dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', - operation: 'terms', - field: 'geo.src', - }, - 1 - ); + await PageObjects.lens.configureDimension({ + dimension: 'lns-layerPanel-1 > lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'geo.src', + }); - await PageObjects.lens.configureDimension( - { - dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', - operation: 'median', - field: 'bytes', - }, - 1 - ); + await PageObjects.lens.configureDimension({ + dimension: 'lns-layerPanel-1 > lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'median', + field: 'bytes', + }); expect(await PageObjects.lens.getLayerCount()).to.eql(2); await PageObjects.lens.removeLayer(); @@ -308,23 +302,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.createLayer(); - await PageObjects.lens.configureDimension( - { - dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', - operation: 'terms', - field: 'geo.src', - }, - 1 - ); + await PageObjects.lens.configureDimension({ + dimension: 'lns-layerPanel-1 > lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'terms', + field: 'geo.src', + }); - await PageObjects.lens.configureDimension( - { - dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', - operation: 'average', - field: 'bytes', - }, - 1 - ); + await PageObjects.lens.configureDimension({ + dimension: 'lns-layerPanel-1 > lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'average', + field: 'bytes', + }); await PageObjects.lens.save('twolayerchart'); await testSubjects.click('lnsSuggestion-asDonut > lnsSuggestion'); diff --git a/x-pack/test/functional/apps/lens/group1/table.ts b/x-pack/test/functional/apps/lens/group1/table.ts index 18ecc2e90cfe4..7bf9b49c53d8d 100644 --- a/x-pack/test/functional/apps/lens/group1/table.ts +++ b/x-pack/test/functional/apps/lens/group1/table.ts @@ -73,10 +73,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should allow to transpose columns', async () => { - await PageObjects.lens.dragDimensionToDimension( - 'lnsDatatable_rows > lns-dimensionTrigger', - 'lnsDatatable_columns > lns-empty-dimension' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsDatatable_rows > lns-dimensionTrigger', + to: 'lnsDatatable_columns > lns-empty-dimension', + }); expect(await PageObjects.lens.getDatatableHeaderText(0)).to.equal('@timestamp per 3 hours'); expect(await PageObjects.lens.getDatatableHeaderText(1)).to.equal( '169.228.188.120 › Average of bytes' diff --git a/x-pack/test/functional/apps/lens/group2/dashboard.ts b/x-pack/test/functional/apps/lens/group2/dashboard.ts index 787a0a6a6d99a..a6f315deba86d 100644 --- a/x-pack/test/functional/apps/lens/group2/dashboard.ts +++ b/x-pack/test/functional/apps/lens/group2/dashboard.ts @@ -183,23 +183,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await PageObjects.lens.hasChartSwitchWarning('line')).to.eql(false); await PageObjects.lens.switchToVisualization('line'); - await PageObjects.lens.configureDimension( - { - dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', - operation: 'date_histogram', - field: '@timestamp', - }, - 1 - ); + await PageObjects.lens.configureDimension({ + dimension: 'lns-layerPanel-1 > lnsXY_xDimensionPanel > lns-empty-dimension', + operation: 'date_histogram', + field: '@timestamp', + }); - await PageObjects.lens.configureDimension( - { - dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', - operation: 'median', - field: 'bytes', - }, - 1 - ); + await PageObjects.lens.configureDimension({ + dimension: 'lns-layerPanel-1 > lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'median', + field: 'bytes', + }); await PageObjects.lens.saveAndReturn(); await panelActions.openContextMenu(); diff --git a/x-pack/test/functional/apps/lens/group3/annotations.ts b/x-pack/test/functional/apps/lens/group3/annotations.ts index 2b641c6c161d4..62e5edb564871 100644 --- a/x-pack/test/functional/apps/lens/group3/annotations.ts +++ b/x-pack/test/functional/apps/lens/group3/annotations.ts @@ -51,10 +51,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should duplicate the style when duplicating an annotation and group them in the chart', async () => { // drag and drop to the empty field to generate a duplicate - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_xAnnotationsPanel > lns-dimensionTrigger', - 'lnsXY_xAnnotationsPanel > lns-empty-dimension' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsXY_xAnnotationsPanel > lns-dimensionTrigger', + to: 'lnsXY_xAnnotationsPanel > lns-empty-dimension', + }); await ( await find.byCssSelector( diff --git a/x-pack/test/functional/apps/lens/group3/drag_and_drop.ts b/x-pack/test/functional/apps/lens/group3/drag_and_drop.ts index dec72008d6f04..626527a9c35cf 100644 --- a/x-pack/test/functional/apps/lens/group3/drag_and_drop.ts +++ b/x-pack/test/functional/apps/lens/group3/drag_and_drop.ts @@ -8,8 +8,10 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; -export default function ({ getPageObjects }: FtrProviderContext) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); + + const listingTable = getService('listingTable'); const xyChartContainer = 'xyVisChart'; describe('lens drag and drop tests', () => { @@ -72,10 +74,10 @@ export default function ({ getPageObjects }: FtrProviderContext) { await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') ).to.eql(['Top 3 values of clientip']); - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_xDimensionPanel > lns-dimensionTrigger', - 'lnsXY_splitDimensionPanel > lns-dimensionTrigger' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lns-layerPanel-0 > lnsXY_xDimensionPanel > lns-dimensionTrigger', + to: 'lns-layerPanel-0 > lnsXY_splitDimensionPanel > lns-dimensionTrigger', + }); expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql( [] @@ -90,10 +92,10 @@ export default function ({ getPageObjects }: FtrProviderContext) { await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') ).to.eql(['Top 3 values of @message.raw']); - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_splitDimensionPanel > lns-dimensionTrigger', - 'lnsXY_yDimensionPanel > lns-dimensionTrigger' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsXY_splitDimensionPanel > lns-dimensionTrigger', + to: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + }); expect( await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') @@ -106,14 +108,14 @@ export default function ({ getPageObjects }: FtrProviderContext) { ]); }); it('should duplicate the column when dragging to empty dimension in the same group', async () => { - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_yDimensionPanel > lns-dimensionTrigger', - 'lnsXY_yDimensionPanel > lns-empty-dimension' - ); - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_yDimensionPanel > lns-dimensionTrigger', - 'lnsXY_yDimensionPanel > lns-empty-dimension' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + to: 'lnsXY_yDimensionPanel > lns-empty-dimension', + }); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + to: 'lnsXY_yDimensionPanel > lns-empty-dimension', + }); expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yDimensionPanel')).to.eql([ 'Unique count of @message.raw', 'Unique count of @message.raw [1]', @@ -121,10 +123,10 @@ export default function ({ getPageObjects }: FtrProviderContext) { ]); }); it('should move duplicated column to non-compatible dimension group', async () => { - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_yDimensionPanel > lns-dimensionTrigger', - 'lnsXY_xDimensionPanel > lns-empty-dimension' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + to: 'lnsXY_xDimensionPanel > lns-empty-dimension', + }); expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yDimensionPanel')).to.eql([ 'Unique count of @message.raw', 'Unique count of @message.raw [1]', @@ -340,5 +342,132 @@ export default function ({ getPageObjects }: FtrProviderContext) { ]); }); }); + + describe('dropping between layers', () => { + it('should move the column', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsXYvis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.createLayer('data'); + + await PageObjects.lens.dragDimensionToExtraDropType( + 'lns-layerPanel-0 > lnsXY_xDimensionPanel > lns-dimensionTrigger', + 'lns-layerPanel-1 > lnsXY_xDimensionPanel', + 'duplicate' + ); + + await PageObjects.lens.assertFocusedDimension('@timestamp [1]'); + + await PageObjects.lens.dragDimensionToExtraDropType( + 'lns-layerPanel-0 > lnsXY_yDimensionPanel > lns-dimensionTrigger', + 'lns-layerPanel-1 > lnsXY_yDimensionPanel', + 'duplicate' + ); + + await PageObjects.lens.assertFocusedDimension('Average of bytes [1]'); + expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-0')).to.eql([ + '@timestamp', + 'Average of bytes', + 'Top values of ip', + ]); + expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-1')).to.eql([ + '@timestamp [1]', + 'Average of bytes [1]', + ]); + }); + + it('should move formula to empty dimension', async () => { + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + operation: 'formula', + formula: `moving_average(average(bytes), window=5`, + }); + await PageObjects.lens.dragDimensionToExtraDropType( + 'lns-layerPanel-0 > lnsXY_yDimensionPanel > lns-dimensionTrigger', + 'lns-layerPanel-1 > lnsXY_yDimensionPanel', + 'duplicate' + ); + + expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-0')).to.eql([ + '@timestamp', + 'moving_average(average(bytes), window=5)', + 'Top 3 values of ip', + ]); + expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-1')).to.eql([ + '@timestamp [1]', + 'moving_average(average(bytes), window=5) [1]', + ]); + }); + + it('should replace formula with another formula', async () => { + await PageObjects.lens.configureDimension({ + dimension: 'lns-layerPanel-1 > lnsXY_yDimensionPanel > lns-dimensionTrigger', + operation: 'formula', + formula: `sum(bytes) + 5`, + }); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lns-layerPanel-0 > lnsXY_yDimensionPanel > lns-dimensionTrigger', + to: 'lns-layerPanel-1 > lnsXY_yDimensionPanel > lns-dimensionTrigger', + }); + expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-0')).to.eql([ + '@timestamp', + 'Top 3 values of ip', + ]); + expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-1')).to.eql([ + '@timestamp [1]', + 'moving_average(average(bytes), window=5)', + ]); + }); + it('swaps dimensions', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('lnsXYvis'); + await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.createLayer('data'); + await PageObjects.lens.dragFieldToDimensionTrigger( + 'bytes', + 'lns-layerPanel-0 > lnsXY_yDimensionPanel > lns-empty-dimension' + ); + await PageObjects.lens.dragFieldToDimensionTrigger( + 'bytes', + 'lns-layerPanel-1 > lnsXY_splitDimensionPanel > lns-empty-dimension' + ); + + await PageObjects.lens.dragDimensionToExtraDropType( + 'lns-layerPanel-1 > lnsXY_splitDimensionPanel > lns-dimensionTrigger', + 'lns-layerPanel-0 > lnsXY_splitDimensionPanel', + 'swap' + ); + + expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-0')).to.eql([ + '@timestamp', + 'Average of bytes', + 'Median of bytes', + 'bytes', + ]); + expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-1')).to.eql([ + 'Top 3 values of ip', + ]); + }); + it('can combine dimensions', async () => { + await PageObjects.lens.dragDimensionToExtraDropType( + 'lns-layerPanel-0 > lnsXY_splitDimensionPanel > lns-dimensionTrigger', + 'lns-layerPanel-1 > lnsXY_splitDimensionPanel', + 'combine' + ); + + expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-0')).to.eql([ + '@timestamp', + 'Average of bytes', + 'Median of bytes', + ]); + expect(await PageObjects.lens.getDimensionTriggersTexts('lns-layerPanel-1')).to.eql([ + 'Top values of ip + 1 other', + ]); + }); + }); }); } diff --git a/x-pack/test/functional/apps/lens/group3/error_handling.ts b/x-pack/test/functional/apps/lens/group3/error_handling.ts index 8f6659bda1562..794547fb96f05 100644 --- a/x-pack/test/functional/apps/lens/group3/error_handling.ts +++ b/x-pack/test/functional/apps/lens/group3/error_handling.ts @@ -56,10 +56,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.waitForMissingDataViewWarning(); await PageObjects.lens.openDimensionEditor('lnsXY_yDimensionPanel > lns-dimensionTrigger'); await PageObjects.lens.closeDimensionEditor(); - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_yDimensionPanel > lns-dimensionTrigger', - 'lnsXY_yDimensionPanel > lns-empty-dimension' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', + to: 'lnsXY_yDimensionPanel > lns-empty-dimension', + }); await PageObjects.lens.switchFirstLayerIndexPattern('log*'); await PageObjects.lens.waitForMissingDataViewWarningDisappear(); await PageObjects.lens.waitForEmptyWorkspace(); diff --git a/x-pack/test/functional/apps/lens/group3/formula.ts b/x-pack/test/functional/apps/lens/group3/formula.ts index 33a24d3aefb1c..806e892cec643 100644 --- a/x-pack/test/functional/apps/lens/group3/formula.ts +++ b/x-pack/test/functional/apps/lens/group3/formula.ts @@ -205,10 +205,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.closeDimensionEditor(); - await PageObjects.lens.dragDimensionToDimension( - 'lnsDatatable_metrics > lns-dimensionTrigger', - 'lnsDatatable_metrics > lns-empty-dimension' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsDatatable_metrics > lns-dimensionTrigger', + to: 'lnsDatatable_metrics > lns-empty-dimension', + }); expect(await PageObjects.lens.getDatatableCellText(1, 1)).to.eql('222,420'); expect(await PageObjects.lens.getDatatableCellText(1, 2)).to.eql('222,420'); }); @@ -249,15 +249,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.createLayer('referenceLine'); - await PageObjects.lens.configureDimension( - { - dimension: 'lnsXY_yReferenceLineLeftPanel > lns-dimensionTrigger', - operation: 'formula', - formula: `count()`, - keepOpen: true, - }, - 1 - ); + await PageObjects.lens.configureDimension({ + dimension: 'lns-layerPanel-1 > lnsXY_yReferenceLineLeftPanel > lns-dimensionTrigger', + operation: 'formula', + formula: `count()`, + keepOpen: true, + }); await PageObjects.lens.switchToStaticValue(); await PageObjects.lens.closeDimensionEditor(); @@ -280,10 +277,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { formula: `0`, }); - await PageObjects.lens.dragDimensionToDimension( - 'lnsDatatable_metrics > lns-dimensionTrigger', - 'lnsDatatable_metrics > lns-empty-dimension' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsDatatable_metrics > lns-dimensionTrigger', + to: 'lnsDatatable_metrics > lns-empty-dimension', + }); expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('0'); expect(await PageObjects.lens.getDatatableCellText(0, 1)).to.eql('0'); }); diff --git a/x-pack/test/functional/apps/lens/group3/reference_lines.ts b/x-pack/test/functional/apps/lens/group3/reference_lines.ts index f022a6cef6e7a..ab2bc48ba57b8 100644 --- a/x-pack/test/functional/apps/lens/group3/reference_lines.ts +++ b/x-pack/test/functional/apps/lens/group3/reference_lines.ts @@ -86,10 +86,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.closeDimensionEditor(); // drag and drop it to the left axis - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_yReferenceLineLeftPanel > lns-dimensionTrigger', - 'lnsXY_yReferenceLineRightPanel > lns-empty-dimension' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsXY_yReferenceLineLeftPanel > lns-dimensionTrigger', + to: 'lnsXY_yReferenceLineRightPanel > lns-empty-dimension', + }); await testSubjects.click('lnsXY_yReferenceLineRightPanel > lns-dimensionTrigger'); expect( @@ -100,10 +100,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should duplicate also the original style when duplicating a reference line', async () => { // drag and drop to the empty field to generate a duplicate - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_yReferenceLineRightPanel > lns-dimensionTrigger', - 'lnsXY_yReferenceLineRightPanel > lns-empty-dimension' - ); + await PageObjects.lens.dragDimensionToDimension({ + from: 'lnsXY_yReferenceLineRightPanel > lns-dimensionTrigger', + to: 'lnsXY_yReferenceLineRightPanel > lns-empty-dimension', + }); await ( await find.byCssSelector( diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index d1125fbb08175..b76cb96e19baa 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -111,22 +111,19 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * @param opts.field - the desired field for the dimension * @param layerIndex - the index of the layer */ - async configureDimension( - opts: { - dimension: string; - operation: string; - field?: string; - isPreviousIncompatible?: boolean; - keepOpen?: boolean; - palette?: string; - formula?: string; - disableEmptyRows?: boolean; - }, - layerIndex = 0 - ) { + async configureDimension(opts: { + dimension: string; + operation: string; + field?: string; + isPreviousIncompatible?: boolean; + keepOpen?: boolean; + palette?: string; + formula?: string; + disableEmptyRows?: boolean; + }) { await retry.try(async () => { if (!(await testSubjects.exists('lns-indexPattern-dimensionContainerClose'))) { - await testSubjects.click(`lns-layerPanel-${layerIndex} > ${opts.dimension}`); + await testSubjects.click(opts.dimension); } await testSubjects.existOrFail('lns-indexPattern-dimensionContainerClose'); }); @@ -450,8 +447,9 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * @param from - the selector of the dimension being moved * @param to - the selector of the dimension being dropped to * */ - async dragDimensionToDimension(from: string, to: string) { + async dragDimensionToDimension({ from, to }: { from: string; to: string }) { await find.existsByCssSelector(from); + await find.existsByCssSelector(to); await browser.html5DragAndDrop( testSubjects.getCssSelector(from), testSubjects.getCssSelector(to) @@ -891,7 +889,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont return dimensionTexts[index]; }, /** - * Gets label of all dimension triggers in dimension group + * Gets label of all dimension triggers in an element * * @param dimension - the selector of the dimension */ From 0ee2ae074e0a7670ec5b2c3b81eae86031d0e85d Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 23 Jun 2022 10:34:29 +0200 Subject: [PATCH 12/54] [Synthetics] Monitor details panel (#134814) Co-authored-by: Abdul Zahid --- .../public/hooks/use_es_search.ts | 30 ++++- .../common/constants/client_defaults.ts | 16 +++ .../hooks/use_status_by_location.tsx | 74 ++++++++++++ .../monitor_summary/monitor_summary.tsx | 15 ++- .../monitor_summary/monitor_summary_tabs.tsx | 2 +- .../tabs_content/locations_status.tsx | 32 +++++ .../tabs_content/monitor_details_panel.tsx | 111 ++++++++++++++++++ .../tabs_content/monitor_tags.tsx | 19 +++ .../tabs_content/summary_tab_content.tsx | 18 ++- .../management/monitor_list_table/columns.tsx | 7 +- .../monitor_list_table/monitor_enabled.tsx | 9 +- .../state/monitor_summary/actions.ts | 6 +- .../synthetics/state/monitor_summary/api.ts | 13 +- .../state/monitor_summary/effects.ts | 15 ++- .../state/monitor_summary/selectors.ts | 2 + .../synthetics_montior_reducer.ts | 38 ++++++ .../apps/synthetics/state/root_effect.ts | 3 +- .../apps/synthetics/state/root_reducer.ts | 2 + .../__mocks__/syncthetics_store.mock.ts | 5 + .../synthetics_service/service_api_client.ts | 49 ++++---- 20 files changed, 422 insertions(+), 44 deletions(-) create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/hooks/use_status_by_location.tsx create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/locations_status.tsx create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/monitor_details_panel.tsx create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/monitor_tags.tsx create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_summary/synthetics_montior_reducer.ts diff --git a/x-pack/plugins/observability/public/hooks/use_es_search.ts b/x-pack/plugins/observability/public/hooks/use_es_search.ts index 2215d31c838fb..493260a3bbb6e 100644 --- a/x-pack/plugins/observability/public/hooks/use_es_search.ts +++ b/x-pack/plugins/observability/public/hooks/use_es_search.ts @@ -9,7 +9,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { ESSearchResponse } from '@kbn/core/types/elasticsearch'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { IInspectorInfo, isCompleteResponse } from '@kbn/data-plugin/common'; +import { IInspectorInfo, isCompleteResponse, isErrorResponse } from '@kbn/data-plugin/common'; import { FETCH_STATUS, useFetcher } from './use_fetcher'; import { useInspectorContext } from '../context/inspector/use_inspector_context'; import { getInspectResponse } from '../../common/utils/get_inspect_response'; @@ -69,6 +69,34 @@ export const useEsSearch = { + if (isErrorResponse(err)) { + console.error(err); + if (addInspectorRequest) { + addInspectorRequest({ + data: { + _inspect: [ + getInspectResponse({ + startTime, + esRequestParams: params, + esResponse: null, + esError: { originalError: err, name: err.name, message: err.message }, + esRequestStatus: 2, + operationName: name, + kibanaRequest: { + route: { + path: '/internal/bsearch', + method: 'POST', + }, + } as any, + }), + ], + }, + status: FETCH_STATUS.SUCCESS, + }); + } + } + }, }); }); } diff --git a/x-pack/plugins/synthetics/common/constants/client_defaults.ts b/x-pack/plugins/synthetics/common/constants/client_defaults.ts index a8860dcca4a1a..5293205a183cb 100644 --- a/x-pack/plugins/synthetics/common/constants/client_defaults.ts +++ b/x-pack/plugins/synthetics/common/constants/client_defaults.ts @@ -5,6 +5,8 @@ * 2.0. */ +import moment from 'moment'; + export const CLIENT_DEFAULTS = { ABSOLUTE_DATE_RANGE_START: 0, // 15 minutes @@ -43,3 +45,17 @@ export const CLIENT_DEFAULTS = { }; export const EXCLUDE_RUN_ONCE_FILTER = { bool: { must_not: { exists: { field: 'run_once' } } } }; +export const SUMMARY_FILTER = { + exists: { + field: 'summary', + }, +}; + +export const getTimeSpanFilter = () => ({ + range: { + 'monitor.timespan': { + lte: moment().toISOString(), + gte: moment().subtract(5, 'minutes').toISOString(), + }, + }, +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/hooks/use_status_by_location.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/hooks/use_status_by_location.tsx new file mode 100644 index 0000000000000..d3da5bc1b35ee --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/hooks/use_status_by_location.tsx @@ -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. + */ + +import { useEsSearch } from '@kbn/observability-plugin/public'; +import { useParams } from 'react-router-dom'; +import { useMemo } from 'react'; +import { Ping } from '../../../../../../common/runtime_types'; +import { + EXCLUDE_RUN_ONCE_FILTER, + getTimeSpanFilter, + SUMMARY_FILTER, +} from '../../../../../../common/constants/client_defaults'; +import { useSyntheticsRefreshContext } from '../../../contexts/synthetics_refresh_context'; +import { SYNTHETICS_INDEX_PATTERN, UNNAMED_LOCATION } from '../../../../../../common/constants'; + +export function useStatusByLocation() { + const { lastRefresh } = useSyntheticsRefreshContext(); + + const { monitorId } = useParams<{ monitorId: string }>(); + + const { data, loading } = useEsSearch( + { + index: SYNTHETICS_INDEX_PATTERN, + body: { + size: 0, + query: { + bool: { + filter: [ + SUMMARY_FILTER, + EXCLUDE_RUN_ONCE_FILTER, + getTimeSpanFilter(), + { + term: { + config_id: monitorId, + }, + }, + ], + }, + }, + sort: [{ '@timestamp': 'desc' }], + aggs: { + locations: { + terms: { + field: 'observer.geo.name', + missing: UNNAMED_LOCATION, + size: 1000, + }, + aggs: { + summary: { + top_hits: { + size: 1, + }, + }, + }, + }, + }, + }, + }, + [lastRefresh, monitorId], + { name: 'getMonitorStatusByLocation' } + ); + + return useMemo(() => { + const locations = (data?.aggregations?.locations.buckets ?? []).map((loc) => { + return loc.summary.hits.hits?.[0]._source as Ping; + }); + + return { locations, loading }; + }, [data, loading]); +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/monitor_summary.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/monitor_summary.tsx index 9ac2d67967eeb..de5871304eccf 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/monitor_summary.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/monitor_summary.tsx @@ -5,14 +5,23 @@ * 2.0. */ -import React from 'react'; -import { useSelector } from 'react-redux'; -import { selectMonitorStatus } from '../../state/monitor_summary'; +import React, { useEffect } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { useParams } from 'react-router-dom'; +import { getSyntheticsMonitorAction, selectMonitorStatus } from '../../state/monitor_summary'; import { useMonitorListBreadcrumbs } from '../monitors_page/hooks/use_breadcrumbs'; export const MonitorSummaryPage = () => { const { data } = useSelector(selectMonitorStatus); useMonitorListBreadcrumbs([{ text: data?.monitor.name ?? '' }]); + const dispatch = useDispatch(); + + const { monitorId } = useParams<{ monitorId: string }>(); + + useEffect(() => { + dispatch(getSyntheticsMonitorAction.get(monitorId)); + }, [dispatch, monitorId]); + return <>; }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/monitor_summary_tabs.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/monitor_summary_tabs.tsx index 3469341d86ca6..725d209c8200e 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/monitor_summary_tabs.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/monitor_summary_tabs.tsx @@ -50,7 +50,7 @@ export const MonitorSummaryTabs = () => { return ( {}} /> diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/locations_status.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/locations_status.tsx new file mode 100644 index 0000000000000..6c918c1288366 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/locations_status.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiBadge, EuiBadgeGroup, EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; +import { useStatusByLocation } from '../hooks/use_status_by_location'; + +export const LocationsStatus = () => { + const { locations, loading } = useStatusByLocation(); + + if (loading) { + return ; + } + + return ( + + {locations.map((loc) => ( + ( + 0 ? 'danger' : 'success'} /> + )} + color="hollow" + > + {loc.observer?.geo?.name} + + ))} + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/monitor_details_panel.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/monitor_details_panel.tsx new file mode 100644 index 0000000000000..8ed2c5dffd771 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/monitor_details_panel.tsx @@ -0,0 +1,111 @@ +/* + * 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 React from 'react'; +import { + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiBadge, + EuiSpacer, + EuiLink, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { capitalize } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { useDispatch, useSelector } from 'react-redux'; +import { useParams } from 'react-router-dom'; +import { MonitorTags } from './monitor_tags'; +import { MonitorEnabled } from '../../monitors_page/management/monitor_list_table/monitor_enabled'; +import { LocationsStatus } from './locations_status'; +import { + getSyntheticsMonitorAction, + selectMonitorStatus, + syntheticsMonitorSelector, +} from '../../../state/monitor_summary'; +import { ConfigKey } from '../../../../../../common/runtime_types'; + +export const MonitorDetailsPanel = () => { + const { data } = useSelector(selectMonitorStatus); + + const { monitorId } = useParams<{ monitorId: string }>(); + + const dispatch = useDispatch(); + + const { data: monitor, loading } = useSelector(syntheticsMonitorSelector); + + if (!data) { + return ; + } + + return ( + <> + + + {ENABLED_LABEL} + + {monitor && ( + { + dispatch(getSyntheticsMonitorAction.get(monitorId)); + }} + /> + )} + + {MONITOR_TYPE_LABEL} + + {capitalize(data.monitor.type)} + + {FREQUENCY_LABEL} + Every 10 mins + {LOCATIONS_LABEL} + + + + {URL_LABEL} + + + {data.url?.full} + + + {TAGS_LABEL} + + {monitor && } + + + + ); +}; + +const FREQUENCY_LABEL = i18n.translate('xpack.synthetics.management.monitorList.frequency', { + defaultMessage: 'Frequency', +}); +const LOCATIONS_LABEL = i18n.translate('xpack.synthetics.management.monitorList.locations', { + defaultMessage: 'Locations', +}); + +const URL_LABEL = i18n.translate('xpack.synthetics.management.monitorList.url', { + defaultMessage: 'URL', +}); + +const TAGS_LABEL = i18n.translate('xpack.synthetics.management.monitorList.tags', { + defaultMessage: 'Tags', +}); + +const ENABLED_LABEL = i18n.translate('xpack.synthetics.detailsPanel.monitorDetails.enabled', { + defaultMessage: 'Enabled', +}); + +const MONITOR_TYPE_LABEL = i18n.translate( + 'xpack.synthetics.detailsPanel.monitorDetails.monitorType', + { + defaultMessage: 'Monitor type', + } +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/monitor_tags.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/monitor_tags.tsx new file mode 100644 index 0000000000000..7960d5efbc1e7 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/monitor_tags.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiBadge, EuiBadgeGroup } from '@elastic/eui'; + +export const MonitorTags = ({ tags }: { tags: string[] }) => { + return ( + + {tags.map((tag) => ( + {tag} + ))} + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/summary_tab_content.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/summary_tab_content.tsx index e426ebbf2e309..c0a2f2c3b30d8 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/summary_tab_content.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/summary_tab_content.tsx @@ -5,9 +5,23 @@ * 2.0. */ -import { EuiText } from '@elastic/eui'; import React from 'react'; +import { EuiTitle, EuiPanel } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { MonitorDetailsPanel } from './monitor_details_panel'; export const SummaryTabContent = () => { - return Monitor summary tab content; + return ( + + +

{MONITOR_DETAILS_LABEL}

+
+ +
+ ); }; + +const MONITOR_DETAILS_LABEL = i18n.translate('xpack.synthetics.detailsPanel.monitorDetails', { + defaultMessage: 'Monitor details', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx index 5665c402442ab..d3803a0aa0260 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx @@ -132,12 +132,7 @@ export function getMonitorListColumns({ defaultMessage: 'Enabled', }), render: (_enabled: boolean, monitor: EncryptedSyntheticsSavedMonitor) => ( - + ), }, { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_enabled.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_enabled.tsx index e98ca2a466f0d..36527f3316935 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_enabled.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_enabled.tsx @@ -10,6 +10,7 @@ import React, { useEffect, useState } from 'react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { FETCH_STATUS, useFetcher } from '@kbn/observability-plugin/public'; +import { useCanEditSynthetics } from '../../../../../../hooks/use_capabilities'; import { ConfigKey, EncryptedSyntheticsMonitor } from '../../../../../../../common/runtime_types'; import { fetchUpsertMonitor } from '../../../../state'; @@ -19,10 +20,12 @@ interface Props { id: string; monitor: EncryptedSyntheticsMonitor; reloadPage: () => void; - isDisabled?: boolean; + initialLoading?: boolean; } -export const MonitorEnabled = ({ id, monitor, reloadPage, isDisabled }: Props) => { +export const MonitorEnabled = ({ id, monitor, reloadPage, initialLoading }: Props) => { + const isDisabled = !useCanEditSynthetics(); + const [isEnabled, setIsEnabled] = useState(null); const { notifications } = useKibana(); @@ -69,7 +72,7 @@ export const MonitorEnabled = ({ id, monitor, reloadPage, isDisabled }: Props) = return ( <> - {isLoading ? ( + {isLoading || initialLoading ? ( ) : ( ( ); export const getMonitorStatusAction = createAsyncAction('[MONITOR SUMMARY] GET'); + +export const getSyntheticsMonitorAction = createAsyncAction( + 'fetchSyntheticsMonitorAction' +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_summary/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_summary/api.ts index 0d0a6c628f03a..af01acf97592d 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_summary/api.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_summary/api.ts @@ -5,9 +5,10 @@ * 2.0. */ +import { SavedObject } from '@kbn/core/types'; import { apiService } from '../../../../utils/api_service'; -import { Ping } from '../../../../../common/runtime_types'; -import { SYNTHETICS_API_URLS } from '../../../../../common/constants'; +import { Ping, SyntheticsMonitor } from '../../../../../common/runtime_types'; +import { API_URLS, SYNTHETICS_API_URLS } from '../../../../../common/constants'; export interface QueryParams { monitorId: string; @@ -18,3 +19,11 @@ export interface QueryParams { export const fetchMonitorStatus = async (params: QueryParams): Promise => { return await apiService.get(SYNTHETICS_API_URLS.MONITOR_STATUS, { ...params }); }; + +export const fetchSyntheticsMonitor = async (monitorId: string): Promise => { + const { attributes } = (await apiService.get( + `${API_URLS.SYNTHETICS_MONITORS}/${monitorId}` + )) as SavedObject; + + return attributes; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_summary/effects.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_summary/effects.ts index dae9f72ac804e..9a1b52e1e24df 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_summary/effects.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_summary/effects.ts @@ -7,8 +7,8 @@ import { takeLeading } from 'redux-saga/effects'; import { fetchEffectFactory } from '../utils/fetch_effect'; -import { getMonitorStatusAction } from './actions'; -import { fetchMonitorStatus } from './api'; +import { getMonitorStatusAction, getSyntheticsMonitorAction } from './actions'; +import { fetchMonitorStatus, fetchSyntheticsMonitor } from './api'; export function* fetchMonitorStatusEffect() { yield takeLeading( @@ -20,3 +20,14 @@ export function* fetchMonitorStatusEffect() { ) ); } + +export function* fetchSyntheticsMonitorEffect() { + yield takeLeading( + getSyntheticsMonitorAction.get, + fetchEffectFactory( + fetchSyntheticsMonitor, + getSyntheticsMonitorAction.success, + getSyntheticsMonitorAction.fail + ) + ); +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_summary/selectors.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_summary/selectors.ts index 09a89a1f07619..d361024e839f2 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_summary/selectors.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_summary/selectors.ts @@ -16,3 +16,5 @@ export const selectSelectedLocationId = createSelector( ); export const selectMonitorStatus = createSelector(getState, (state) => state); + +export const syntheticsMonitorSelector = (state: SyntheticsAppState) => state.syntheticsMonitor; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_summary/synthetics_montior_reducer.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_summary/synthetics_montior_reducer.ts new file mode 100644 index 0000000000000..e1049c3b862d3 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_summary/synthetics_montior_reducer.ts @@ -0,0 +1,38 @@ +/* + * 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 { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; +import { createReducer } from '@reduxjs/toolkit'; +import { SyntheticsMonitor } from '../../../../../common/runtime_types'; +import { getSyntheticsMonitorAction } from './actions'; + +export interface SyntheticsMonitorState { + data: SyntheticsMonitor | null; + loading: boolean; + error: IHttpFetchError | null; +} + +const initialState: SyntheticsMonitorState = { + data: null, + loading: false, + error: null, +}; + +export const syntheticsMonitorReducer = createReducer(initialState, (builder) => { + builder + .addCase(getSyntheticsMonitorAction.get, (state) => { + state.loading = true; + }) + .addCase(getSyntheticsMonitorAction.success, (state, action) => { + state.data = action.payload; + state.loading = false; + }) + .addCase(getSyntheticsMonitorAction.fail, (state, action) => { + state.error = action.payload as IHttpFetchError; + state.loading = false; + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts index 9cae9249af971..45214cf4d2461 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts @@ -6,7 +6,7 @@ */ import { all, fork } from 'redux-saga/effects'; -import { fetchMonitorStatusEffect } from './monitor_summary'; +import { fetchMonitorStatusEffect, fetchSyntheticsMonitorEffect } from './monitor_summary'; import { fetchIndexStatusEffect } from './index_status'; import { fetchSyntheticsEnablementEffect } from './synthetics_enablement'; import { fetchMonitorListEffect } from './monitor_list'; @@ -19,5 +19,6 @@ export const rootEffect = function* root(): Generator { fork(fetchServiceLocationsEffect), fork(fetchMonitorListEffect), fork(fetchMonitorStatusEffect), + fork(fetchSyntheticsMonitorEffect), ]); }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts index 4ecd0dbc265ab..bd4b25b456e93 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts @@ -7,6 +7,7 @@ import { combineReducers } from '@reduxjs/toolkit'; +import { syntheticsMonitorReducer } from './monitor_summary/synthetics_montior_reducer'; import { monitorStatusReducer } from './monitor_summary'; import { uiReducer } from './ui'; import { indexStatusReducer } from './index_status'; @@ -21,6 +22,7 @@ export const rootReducer = combineReducers({ monitorList: monitorListReducer, serviceLocations: serviceLocationsReducer, monitorStatus: monitorStatusReducer, + syntheticsMonitor: syntheticsMonitorReducer, }); export type SyntheticsAppState = ReturnType; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/syncthetics_store.mock.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/syncthetics_store.mock.ts index c5aad9ffa01ac..3a9c13f928a76 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/syncthetics_store.mock.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/syncthetics_store.mock.ts @@ -84,4 +84,9 @@ export const mockState: SyntheticsAppState = { error: null, selectedLocationId: null, }, + syntheticsMonitor: { + data: null, + loading: false, + error: null, + }, }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts index 02d0f69ddbedc..235055b8c2b38 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts @@ -55,8 +55,11 @@ export class ServiceAPIClient { this.server = server; } - getHttpsAgent() { + getHttpsAgent(url: string) { const config = this.config; + if (url !== this.config.devUrl && this.authorization && this.server.isDev) { + return; + } if (config.tls && config.tls.certificate && config.tls.key) { const tlsConfig = new SslConfig(config.tls); @@ -92,29 +95,31 @@ export class ServiceAPIClient { return { allowed: true, signupUrl: null }; } - const httpsAgent = this.getHttpsAgent(); - - if (this.locations.length > 0 && httpsAgent) { + if (this.locations.length > 0) { // get a url from a random location const url = this.locations[Math.floor(Math.random() * this.locations.length)].url; - try { - const { data } = await axios({ - method: 'GET', - url: url + '/allowed', - headers: - process.env.NODE_ENV !== 'production' && this.authorization - ? { - Authorization: this.authorization, - } - : undefined, - httpsAgent, - }); - - const { allowed, signupUrl } = data; - return { allowed, signupUrl }; - } catch (e) { - this.logger.error(e); + const httpsAgent = this.getHttpsAgent(url); + + if (httpsAgent) { + try { + const { data } = await axios({ + method: 'GET', + url: url + '/allowed', + headers: + process.env.NODE_ENV !== 'production' && this.authorization + ? { + Authorization: this.authorization, + } + : undefined, + httpsAgent, + }); + + const { allowed, signupUrl } = data; + return { allowed, signupUrl }; + } catch (e) { + this.logger.error(e); + } } } @@ -151,7 +156,7 @@ export class ServiceAPIClient { Authorization: this.authorization, } : undefined, - httpsAgent: this.getHttpsAgent(), + httpsAgent: this.getHttpsAgent(url), }); }; From adbd6a5fb8a172a7e53273a7dbafd3aec1bb74f3 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 23 Jun 2022 10:38:23 +0200 Subject: [PATCH 13/54] [ML] @kbn/ml-agg-utils, @kbn/ml-is-populated-object, @kbn/ml-string-hash packages. (#132963) Moves some ML utility code to packages. - @kbn/ml-agg-utils contains multiple utilities used in combination related to building aggregations. - @kbn/ml-is-populated-object contains the isPopulatedObject() utility function used across several plugins. - @kbn/ml-string-hash contains the stringHash() utility function used across several plugins. --- .github/CODEOWNERS | 1 + package.json | 6 + packages/BUILD.bazel | 6 + .../src/bazel_package_dirs.ts | 1 + x-pack/packages/ml/agg_utils/BUILD.bazel | 125 ++++++++ x-pack/packages/ml/agg_utils/README.md | 32 ++ x-pack/packages/ml/agg_utils/jest.config.js | 12 + x-pack/packages/ml/agg_utils/package.json | 13 + .../src/build_sampler_aggregation.test.ts | 31 ++ .../src/build_sampler_aggregation.ts | 31 ++ .../ml/agg_utils/src/get_agg_intervals.ts | 107 +++++++ ...sampler_aggregations_response_path.test.ts | 18 ++ .../get_sampler_aggregations_response_path.ts | 14 + x-pack/packages/ml/agg_utils/src/index.ts | 10 + x-pack/packages/ml/agg_utils/tsconfig.json | 17 + x-pack/packages/ml/agg_utils/yarn.lock | 300 ++++++++++++++++++ .../ml/is_populated_object/BUILD.bazel | 114 +++++++ .../packages/ml/is_populated_object/README.md | 24 ++ .../ml/is_populated_object/jest.config.js | 12 + .../ml/is_populated_object/package.json | 13 + .../ml/is_populated_object/src/index.ts | 8 + .../src/is_populated_object.test.ts | 48 +++ .../src/is_populated_object.ts} | 2 +- .../ml/is_populated_object/tsconfig.json | 17 + .../packages/ml/is_populated_object/yarn.lock | 300 ++++++++++++++++++ x-pack/packages/ml/string_hash/BUILD.bazel | 114 +++++++ x-pack/packages/ml/string_hash/README.md | 15 + x-pack/packages/ml/string_hash/jest.config.js | 12 + x-pack/packages/ml/string_hash/package.json | 13 + x-pack/packages/ml/string_hash/src/index.ts | 8 + .../ml/string_hash/src/string_hash.test.ts | 16 + .../ml/string_hash/src/string_hash.ts} | 0 x-pack/packages/ml/string_hash/tsconfig.json | 17 + x-pack/packages/ml/string_hash/yarn.lock | 300 ++++++++++++++++++ .../common/types/field_stats.ts | 2 +- .../data_visualizer/common/types/index.ts | 2 +- .../common/utils/query_utils.ts | 29 -- .../common/utils/runtime_field_utils.ts | 2 +- .../full_time_range_selector_service.ts | 2 +- .../requests/get_boolean_field_stats.ts | 8 +- .../requests/get_date_field_stats.ts | 7 +- .../requests/get_document_stats.ts | 2 +- .../requests/get_field_examples.ts | 2 +- .../requests/get_numeric_field_stats.ts | 7 +- .../requests/get_string_field_stats.ts | 7 +- .../search_strategy/requests/overall_stats.ts | 5 +- .../utils/error_utils.ts | 2 +- .../utils/query_utils.ts | 2 +- x-pack/plugins/file_upload/common/utils.ts | 19 -- .../file_upload/public/importer/importer.ts | 2 +- .../server/get_time_field_range.ts | 2 +- .../server/utils/runtime_field_utils.ts | 2 +- x-pack/plugins/ml/common/index.ts | 1 - x-pack/plugins/ml/common/types/es_client.ts | 2 +- .../ml/common/types/feature_importance.ts | 2 +- .../ml/common/util/errors/process_errors.ts | 2 +- .../ml/common/util/group_color_utils.ts | 2 +- x-pack/plugins/ml/common/util/job_utils.ts | 4 +- .../ml/common/util/object_utils.test.ts | 42 +-- x-pack/plugins/ml/common/util/object_utils.ts | 30 +- x-pack/plugins/ml/common/util/query_utils.ts | 2 +- .../ml/common/util/runtime_field_utils.ts | 2 +- .../ml/common/util/string_utils.test.ts | 15 +- x-pack/plugins/ml/common/util/string_utils.ts | 17 - x-pack/plugins/ml/common/util/validators.ts | 2 +- ...aly_detection_jobs_health_rule_trigger.tsx | 2 +- .../components/data_grid/data_grid.tsx | 2 +- .../full_time_range_selector_service.ts | 2 +- .../scatterplot_matrix/scatterplot_matrix.tsx | 2 +- .../runtime_mappings/runtime_mappings.tsx | 2 +- .../hooks/use_exploration_url_state.ts | 2 +- .../jobs/jobs_list/components/utils.js | 2 +- .../ml/public/application/jobs/jobs_utils.ts | 2 +- .../util/filter_runtime_mappings.ts | 2 +- .../jobs/new_job/recognize/page.tsx | 2 +- .../anomaly_explorer_charts_service.ts | 2 +- .../services/anomaly_timeline_service.ts | 2 +- .../results_service/result_service_rx.ts | 2 +- .../results_service/results_service.js | 3 +- .../models_management/expanded_row.tsx | 2 +- .../models_management/models_list.tsx | 2 +- .../models_management/test_models/utils.ts | 3 +- .../ml/public/application/util/url_state.tsx | 2 +- x-pack/plugins/ml/public/embeddables/types.ts | 2 +- .../plugins/ml/server/lib/query_utils.test.ts | 39 +-- x-pack/plugins/ml/server/lib/query_utils.ts | 30 -- .../models/data_recognizer/data_recognizer.ts | 2 +- .../models/data_visualizer/data_visualizer.ts | 86 +---- .../models/fields_service/fields_service.ts | 2 +- .../ml/server/models/job_service/jobs.ts | 2 +- .../models/results_service/anomaly_charts.ts | 3 +- .../common/api_schemas/type_guards.ts | 3 +- .../transform/common/shared_imports.ts | 1 - .../transform/common/types/data_view.ts | 3 +- .../transform/common/types/transform.ts | 2 +- .../transform/common/types/transform_stats.ts | 3 +- .../plugins/transform/common/utils/errors.ts | 2 +- .../transform/public/app/common/pivot_aggs.ts | 2 +- .../public/app/common/pivot_group_by.ts | 2 +- .../transform/public/app/common/request.ts | 2 +- .../lib/authorization/components/common.ts | 2 +- .../step_create/step_create_form.tsx | 2 +- .../filter_agg/components/filter_agg_form.tsx | 2 +- .../common/top_metrics_agg/config.ts | 2 +- .../components/step_define/common/types.ts | 3 +- .../use_edit_transform_flyout.ts | 2 +- .../transform_list/expanded_row.tsx | 18 +- .../server/routes/api/transforms_nodes.ts | 3 +- .../apis/ml/modules/get_module.ts | 2 +- yarn.lock | 24 ++ 110 files changed, 1871 insertions(+), 397 deletions(-) create mode 100644 x-pack/packages/ml/agg_utils/BUILD.bazel create mode 100644 x-pack/packages/ml/agg_utils/README.md create mode 100644 x-pack/packages/ml/agg_utils/jest.config.js create mode 100644 x-pack/packages/ml/agg_utils/package.json create mode 100644 x-pack/packages/ml/agg_utils/src/build_sampler_aggregation.test.ts create mode 100644 x-pack/packages/ml/agg_utils/src/build_sampler_aggregation.ts create mode 100644 x-pack/packages/ml/agg_utils/src/get_agg_intervals.ts create mode 100644 x-pack/packages/ml/agg_utils/src/get_sampler_aggregations_response_path.test.ts create mode 100644 x-pack/packages/ml/agg_utils/src/get_sampler_aggregations_response_path.ts create mode 100644 x-pack/packages/ml/agg_utils/src/index.ts create mode 100644 x-pack/packages/ml/agg_utils/tsconfig.json create mode 100644 x-pack/packages/ml/agg_utils/yarn.lock create mode 100644 x-pack/packages/ml/is_populated_object/BUILD.bazel create mode 100644 x-pack/packages/ml/is_populated_object/README.md create mode 100644 x-pack/packages/ml/is_populated_object/jest.config.js create mode 100644 x-pack/packages/ml/is_populated_object/package.json create mode 100644 x-pack/packages/ml/is_populated_object/src/index.ts create mode 100644 x-pack/packages/ml/is_populated_object/src/is_populated_object.test.ts rename x-pack/{plugins/data_visualizer/common/utils/object_utils.ts => packages/ml/is_populated_object/src/is_populated_object.ts} (99%) create mode 100644 x-pack/packages/ml/is_populated_object/tsconfig.json create mode 100644 x-pack/packages/ml/is_populated_object/yarn.lock create mode 100644 x-pack/packages/ml/string_hash/BUILD.bazel create mode 100644 x-pack/packages/ml/string_hash/README.md create mode 100644 x-pack/packages/ml/string_hash/jest.config.js create mode 100644 x-pack/packages/ml/string_hash/package.json create mode 100644 x-pack/packages/ml/string_hash/src/index.ts create mode 100644 x-pack/packages/ml/string_hash/src/string_hash.test.ts rename x-pack/{plugins/data_visualizer/common/utils/string_utils.ts => packages/ml/string_hash/src/string_hash.ts} (100%) create mode 100644 x-pack/packages/ml/string_hash/tsconfig.json create mode 100644 x-pack/packages/ml/string_hash/yarn.lock delete mode 100644 x-pack/plugins/file_upload/common/utils.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2682f1fc9c7c9..6813dcaa33c09 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -202,6 +202,7 @@ /x-pack/test/functional/apps/transform/ @elastic/ml-ui /x-pack/test/functional/services/transform/ @elastic/ml-ui /x-pack/test/functional_basic/apps/transform/ @elastic/ml-ui +/x-pack/packages/ml/ @elastic/ml-ui /packages/kbn-aiops-utils @elastic/ml-ui /examples/response_stream/ @elastic/ml-ui diff --git a/package.json b/package.json index 14c0d5e1b0aa1..717392523c956 100644 --- a/package.json +++ b/package.json @@ -197,6 +197,9 @@ "@kbn/logging": "link:bazel-bin/packages/kbn-logging", "@kbn/logging-mocks": "link:bazel-bin/packages/kbn-logging-mocks", "@kbn/mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl", + "@kbn/ml-agg-utils": "link:bazel-bin/x-pack/packages/ml/agg_utils", + "@kbn/ml-is-populated-object": "link:bazel-bin/x-pack/packages/ml/is_populated_object", + "@kbn/ml-string-hash": "link:bazel-bin/x-pack/packages/ml/string_hash", "@kbn/monaco": "link:bazel-bin/packages/kbn-monaco", "@kbn/plugin-discovery": "link:bazel-bin/packages/kbn-plugin-discovery", "@kbn/react-field": "link:bazel-bin/packages/kbn-react-field", @@ -743,6 +746,9 @@ "@types/kbn__logging": "link:bazel-bin/packages/kbn-logging/npm_module_types", "@types/kbn__logging-mocks": "link:bazel-bin/packages/kbn-logging-mocks/npm_module_types", "@types/kbn__mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl/npm_module_types", + "@types/kbn__ml-agg-utils": "link:bazel-bin/x-pack/packages/ml/agg_utils/npm_module_types", + "@types/kbn__ml-is-populated-object": "link:bazel-bin/x-pack/packages/ml/is_populated_object/npm_module_types", + "@types/kbn__ml-string-hash": "link:bazel-bin/x-pack/packages/ml/string_hash/npm_module_types", "@types/kbn__monaco": "link:bazel-bin/packages/kbn-monaco/npm_module_types", "@types/kbn__optimizer": "link:bazel-bin/packages/kbn-optimizer/npm_module_types", "@types/kbn__optimizer-webpack-helpers": "link:bazel-bin/packages/kbn-optimizer-webpack-helpers/npm_module_types", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 2cce534bb98ef..253c5cc2b4fb3 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -159,6 +159,9 @@ filegroup( "//packages/shared-ux/page/analytics_no_data:build", "//packages/shared-ux/page/kibana_no_data:build", "//packages/shared-ux/prompt/no_data_views:build", + "//x-pack/packages/ml/agg_utils:build", + "//x-pack/packages/ml/is_populated_object:build", + "//x-pack/packages/ml/string_hash:build", ], ) @@ -301,6 +304,9 @@ filegroup( "//packages/shared-ux/page/analytics_no_data:build_types", "//packages/shared-ux/page/kibana_no_data:build_types", "//packages/shared-ux/prompt/no_data_views:build_types", + "//x-pack/packages/ml/agg_utils:build_types", + "//x-pack/packages/ml/is_populated_object:build_types", + "//x-pack/packages/ml/string_hash:build_types", ], ) diff --git a/packages/kbn-bazel-packages/src/bazel_package_dirs.ts b/packages/kbn-bazel-packages/src/bazel_package_dirs.ts index 5c0bfb5d4595b..ed295cffd7ede 100644 --- a/packages/kbn-bazel-packages/src/bazel_package_dirs.ts +++ b/packages/kbn-bazel-packages/src/bazel_package_dirs.ts @@ -27,6 +27,7 @@ export const BAZEL_PACKAGE_DIRS = [ 'packages/analytics/shippers', 'packages/analytics/shippers/elastic_v3', 'packages/core/*', + 'x-pack/packages/ml', ]; /** diff --git a/x-pack/packages/ml/agg_utils/BUILD.bazel b/x-pack/packages/ml/agg_utils/BUILD.bazel new file mode 100644 index 0000000000000..0d59aca092fd5 --- /dev/null +++ b/x-pack/packages/ml/agg_utils/BUILD.bazel @@ -0,0 +1,125 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "agg_utils" +PKG_REQUIRE_NAME = "@kbn/ml-agg-utils" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "@npm//@elastic/elasticsearch", + "@npm//lodash", + "//packages/kbn-field-types", + "//x-pack/packages/ml/is_populated_object", + "//x-pack/packages/ml/string_hash", +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@elastic/elasticsearch", + "@npm//tslib", + "//packages/kbn-field-types:npm_module_types", + "//x-pack/packages/ml/is_populated_object:npm_module_types", + "//x-pack/packages/ml/string_hash:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/x-pack/packages/ml/agg_utils/README.md b/x-pack/packages/ml/agg_utils/README.md new file mode 100644 index 0000000000000..63a30e1f1cbef --- /dev/null +++ b/x-pack/packages/ml/agg_utils/README.md @@ -0,0 +1,32 @@ +# @kbn/ml-agg-utils + +This package includes utility functions provided by the ML team to be used in Kibana plugins related to data manipulation and verification. + + + +### `buildSamplerAggregation` (function) + +Wraps the supplied aggregations in a sampler aggregation. +A supplied samplerShardSize (the shard_size parameter of the sampler aggregation) +of less than 1 indicates no sampling, and the aggs are returned as-is. + +**Parameters:** + +- aggs (`any`) +- samplerShardSize (`number`) + +**returns:** Record + +### `getSamplerAggregationsResponsePath` (function) + +**Parameters:** + +- samplerShardSize (`number`) + +**returns:** string[] + +### `getAggIntervals` (function) + +Returns aggregation intervals for the supplied document fields. + + diff --git a/x-pack/packages/ml/agg_utils/jest.config.js b/x-pack/packages/ml/agg_utils/jest.config.js new file mode 100644 index 0000000000000..a22a76d5bf951 --- /dev/null +++ b/x-pack/packages/ml/agg_utils/jest.config.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../..', + roots: ['/x-pack/packages/ml/agg_utils'], +}; diff --git a/x-pack/packages/ml/agg_utils/package.json b/x-pack/packages/ml/agg_utils/package.json new file mode 100644 index 0000000000000..11f2fe9d4d450 --- /dev/null +++ b/x-pack/packages/ml/agg_utils/package.json @@ -0,0 +1,13 @@ +{ + "name": "@kbn/ml-agg-utils", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0", + "devDependencies": { + "ts-readme": "^1.1.3" + }, + "scripts": { + "generate-docs": "ts-readme src/index.ts" + } +} diff --git a/x-pack/packages/ml/agg_utils/src/build_sampler_aggregation.test.ts b/x-pack/packages/ml/agg_utils/src/build_sampler_aggregation.test.ts new file mode 100644 index 0000000000000..c792b331ef5b2 --- /dev/null +++ b/x-pack/packages/ml/agg_utils/src/build_sampler_aggregation.test.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 { buildSamplerAggregation } from './build_sampler_aggregation'; + +describe('buildSamplerAggregation', () => { + const testAggs = { + bytes_stats: { + stats: { field: 'bytes' }, + }, + }; + + test('returns wrapped sampler aggregation for sampler shard size of 1000', () => { + expect(buildSamplerAggregation(testAggs, 1000)).toEqual({ + sample: { + sampler: { + shard_size: 1000, + }, + aggs: testAggs, + }, + }); + }); + + test('returns un-sampled aggregation as-is for sampler shard size of 0', () => { + expect(buildSamplerAggregation(testAggs, 0)).toEqual(testAggs); + }); +}); diff --git a/x-pack/packages/ml/agg_utils/src/build_sampler_aggregation.ts b/x-pack/packages/ml/agg_utils/src/build_sampler_aggregation.ts new file mode 100644 index 0000000000000..30345b00caf2f --- /dev/null +++ b/x-pack/packages/ml/agg_utils/src/build_sampler_aggregation.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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +/** + * Wraps the supplied aggregations in a sampler aggregation. + * A supplied samplerShardSize (the shard_size parameter of the sampler aggregation) + * of less than 1 indicates no sampling, and the aggs are returned as-is. + */ +export function buildSamplerAggregation( + aggs: any, + samplerShardSize: number +): Record { + if (samplerShardSize < 1) { + return aggs; + } + + return { + sample: { + sampler: { + shard_size: samplerShardSize, + }, + aggs, + }, + }; +} diff --git a/x-pack/packages/ml/agg_utils/src/get_agg_intervals.ts b/x-pack/packages/ml/agg_utils/src/get_agg_intervals.ts new file mode 100644 index 0000000000000..67a6f28497d6e --- /dev/null +++ b/x-pack/packages/ml/agg_utils/src/get_agg_intervals.ts @@ -0,0 +1,107 @@ +/* + * 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 { get } from 'lodash'; + +import type { Client } from '@elastic/elasticsearch'; +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import { KBN_FIELD_TYPES } from '@kbn/field-types'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { stringHash } from '@kbn/ml-string-hash'; + +import { buildSamplerAggregation } from './build_sampler_aggregation'; +import { getSamplerAggregationsResponsePath } from './get_sampler_aggregations_response_path'; + +// TODO Temporary type definition until we can import from `@kbn/core`. +// Copied from src/core/server/elasticsearch/client/types.ts +// as these types aren't part of any package yet. Once they are, remove this completely + +/** + * Client used to query the elasticsearch cluster. + * @deprecated At some point use the one from src/core/server/elasticsearch/client/types.ts when it is made into a package. If it never is, then keep using this one. + * @public + */ +type ElasticsearchClient = Omit< + Client, + 'connectionPool' | 'serializer' | 'extend' | 'close' | 'diagnostic' +>; + +const MAX_CHART_COLUMNS = 20; + +interface HistogramField { + fieldName: string; + type: string; +} + +interface NumericColumnStats { + interval: number; + min: number; + max: number; +} +type NumericColumnStatsMap = Record; + +/** + * Returns aggregation intervals for the supplied document fields. + */ +export const getAggIntervals = async ( + client: ElasticsearchClient, + indexPattern: string, + query: estypes.QueryDslQueryContainer, + fields: HistogramField[], + samplerShardSize: number, + runtimeMappings?: estypes.MappingRuntimeFields +): Promise => { + const numericColumns = fields.filter((field) => { + return field.type === KBN_FIELD_TYPES.NUMBER || field.type === KBN_FIELD_TYPES.DATE; + }); + + if (numericColumns.length === 0) { + return {}; + } + + const minMaxAggs = numericColumns.reduce((aggs, c) => { + const id = stringHash(c.fieldName); + aggs[id] = { + stats: { + field: c.fieldName, + }, + }; + return aggs; + }, {} as Record); + + const body = await client.search({ + index: indexPattern, + size: 0, + body: { + query, + aggs: buildSamplerAggregation(minMaxAggs, samplerShardSize), + size: 0, + ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), + }, + }); + + const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); + const aggregations = aggsPath.length > 0 ? get(body.aggregations, aggsPath) : body.aggregations; + + return Object.keys(aggregations).reduce((p, aggName) => { + const stats = [aggregations[aggName].min, aggregations[aggName].max]; + if (!stats.includes(null)) { + const delta = aggregations[aggName].max - aggregations[aggName].min; + + let aggInterval = 1; + + if (delta > MAX_CHART_COLUMNS || delta <= 1) { + aggInterval = delta / (MAX_CHART_COLUMNS - 1); + } + + p[aggName] = { interval: aggInterval, min: stats[0], max: stats[1] }; + } + + return p; + }, {} as NumericColumnStatsMap); +}; diff --git a/x-pack/packages/ml/agg_utils/src/get_sampler_aggregations_response_path.test.ts b/x-pack/packages/ml/agg_utils/src/get_sampler_aggregations_response_path.test.ts new file mode 100644 index 0000000000000..78b30d27aada0 --- /dev/null +++ b/x-pack/packages/ml/agg_utils/src/get_sampler_aggregations_response_path.test.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 { getSamplerAggregationsResponsePath } from './get_sampler_aggregations_response_path'; + +describe('getSamplerAggregationsResponsePath', () => { + test('returns correct path for sampler shard size of 1000', () => { + expect(getSamplerAggregationsResponsePath(1000)).toEqual(['sample']); + }); + + test('returns correct path for sampler shard size of 0', () => { + expect(getSamplerAggregationsResponsePath(0)).toEqual([]); + }); +}); diff --git a/x-pack/packages/ml/agg_utils/src/get_sampler_aggregations_response_path.ts b/x-pack/packages/ml/agg_utils/src/get_sampler_aggregations_response_path.ts new file mode 100644 index 0000000000000..48a9e5051cacd --- /dev/null +++ b/x-pack/packages/ml/agg_utils/src/get_sampler_aggregations_response_path.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. + */ + +// Returns the path of aggregations in the elasticsearch response, as an array, +// depending on whether sampling is being used. +// A supplied samplerShardSize (the shard_size parameter of the sampler aggregation) +// of less than 1 indicates no sampling, and an empty array is returned. +export function getSamplerAggregationsResponsePath(samplerShardSize: number): string[] { + return samplerShardSize > 0 ? ['sample'] : []; +} diff --git a/x-pack/packages/ml/agg_utils/src/index.ts b/x-pack/packages/ml/agg_utils/src/index.ts new file mode 100644 index 0000000000000..6705a28579b40 --- /dev/null +++ b/x-pack/packages/ml/agg_utils/src/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { buildSamplerAggregation } from './build_sampler_aggregation'; +export { getAggIntervals } from './get_agg_intervals'; +export { getSamplerAggregationsResponsePath } from './get_sampler_aggregations_response_path'; diff --git a/x-pack/packages/ml/agg_utils/tsconfig.json b/x-pack/packages/ml/agg_utils/tsconfig.json new file mode 100644 index 0000000000000..b74cfcda5ee73 --- /dev/null +++ b/x-pack/packages/ml/agg_utils/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node", + ], + }, + "include": [ + "src/**/*", + ] +} diff --git a/x-pack/packages/ml/agg_utils/yarn.lock b/x-pack/packages/ml/agg_utils/yarn.lock new file mode 100644 index 0000000000000..e826cf14c9da2 --- /dev/null +++ b/x-pack/packages/ml/agg_utils/yarn.lock @@ -0,0 +1,300 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@types/command-line-args@^5.0.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@types/command-line-args/-/command-line-args-5.2.0.tgz#adbb77980a1cc376bb208e3f4142e907410430f6" + integrity sha512-UuKzKpJJ/Ief6ufIaIzr3A/0XnluX7RvFgwkV89Yzvm77wCh1kFaFmqN8XEnGcN62EuHdedQjEMb8mYxFLGPyA== + +"@types/command-line-usage@^5.0.1": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@types/command-line-usage/-/command-line-usage-5.0.2.tgz#ba5e3f6ae5a2009d466679cc431b50635bf1a064" + integrity sha512-n7RlEEJ+4x4TS7ZQddTmNSxP+zziEG0TNsMfiRIxcIVXt71ENJ9ojeXmGO3wPoTdn7pJcU2xc3CJYMktNT6DPg== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +array-back@^3.0.1, array-back@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" + integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== + +array-back@^4.0.1, array-back@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e" + integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== + +braces@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +command-line-application@^0.9.6: + version "0.9.6" + resolved "https://registry.yarnpkg.com/command-line-application/-/command-line-application-0.9.6.tgz#03da3db29a0dbee1af601f03198a2f2425d67803" + integrity sha512-7wc7YX7s/hqZWKp4r37IBlW/Bhh92HWeQW2VV++Mt9x35AKFntz9f7A94Zz+AsImHZmRGHd8iNW5m0jUd4GQpg== + dependencies: + "@types/command-line-args" "^5.0.0" + "@types/command-line-usage" "^5.0.1" + chalk "^2.4.1" + command-line-args "^5.1.1" + command-line-usage "^6.0.0" + meant "^1.0.1" + remove-markdown "^0.3.0" + tslib "1.10.0" + +command-line-args@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e" + integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg== + dependencies: + array-back "^3.1.0" + find-replace "^3.0.0" + lodash.camelcase "^4.3.0" + typical "^4.0.0" + +command-line-usage@^6.0.0: + version "6.1.3" + resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.3.tgz#428fa5acde6a838779dfa30e44686f4b6761d957" + integrity sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw== + dependencies: + array-back "^4.0.2" + chalk "^2.4.2" + table-layout "^1.0.2" + typical "^5.2.0" + +deep-extend@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +fast-glob@^3.1.1: + version "3.2.11" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" + integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fastq@^1.6.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + dependencies: + reusify "^1.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-replace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" + integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== + dependencies: + array-back "^3.0.1" + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + +meant@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/meant/-/meant-1.0.3.tgz#67769af9de1d158773e928ae82c456114903554c" + integrity sha512-88ZRGcNxAq4EH38cQ4D85PM57pikCwS8Z99EWHODxN7KBY+UuPiqzRTtZzS8KTXO/ywSWbdjjJST2Hly/EQxLw== + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +prettier@1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" + integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +reduce-flatten@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" + integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== + +remove-markdown@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/remove-markdown/-/remove-markdown-0.3.0.tgz#5e4b667493a93579728f3d52ecc1db9ca505dc98" + integrity sha512-5392eIuy1mhjM74739VunOlsOYKjsH82rQcTBlJ1bkICVC3dQ3ksQzTHh4jGHQFnM+1xzLzcFOMH+BofqXhroQ== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +table-layout@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.2.tgz#c4038a1853b0136d63365a734b6931cf4fad4a04" + integrity sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A== + dependencies: + array-back "^4.0.1" + deep-extend "~0.6.0" + typical "^5.2.0" + wordwrapjs "^4.0.0" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +ts-readme@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/ts-readme/-/ts-readme-1.1.3.tgz#18a73d21f3bb50ee8e2df819bcbbe3a76385b15a" + integrity sha512-GvI+Vu3m/LGBlgrWwzSmvslnz8msJLNrZ7hQ3Ko2B6PMxeXidqsn6fi20IWgepFjOzhKGw/WlG8NmM7jl3DWeg== + dependencies: + command-line-application "^0.9.6" + fast-glob "^3.1.1" + prettier "1.19.1" + +tslib@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + +typical@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" + integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== + +typical@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066" + integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg== + +wordwrapjs@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.1.tgz#d9790bccfb110a0fc7836b5ebce0937b37a8b98f" + integrity sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA== + dependencies: + reduce-flatten "^2.0.0" + typical "^5.2.0" diff --git a/x-pack/packages/ml/is_populated_object/BUILD.bazel b/x-pack/packages/ml/is_populated_object/BUILD.bazel new file mode 100644 index 0000000000000..e89b39af7c986 --- /dev/null +++ b/x-pack/packages/ml/is_populated_object/BUILD.bazel @@ -0,0 +1,114 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "is_populated_object" +PKG_REQUIRE_NAME = "@kbn/ml-is-populated-object" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/x-pack/packages/ml/is_populated_object/README.md b/x-pack/packages/ml/is_populated_object/README.md new file mode 100644 index 0000000000000..8d2d47329a3fa --- /dev/null +++ b/x-pack/packages/ml/is_populated_object/README.md @@ -0,0 +1,24 @@ +# @kbn/ml-is-populated-object + + + +### `isPopulatedObject` (function) + +A type guard to check record like object structures. + +Examples: + +- `isPopulatedObject({...})` + Limits type to Record + +- `isPopulatedObject({...}, ['attribute'])` + Limits type to Record<'attribute', unknown> + +- `isPopulatedObject({...})` + Limits type to a record with keys of the given interface. + Note that you might want to add keys from the interface to the + array of requiredAttributes to satisfy runtime requirements. + Otherwise you'd just satisfy TS requirements but might still + run into runtime issues. + + diff --git a/x-pack/packages/ml/is_populated_object/jest.config.js b/x-pack/packages/ml/is_populated_object/jest.config.js new file mode 100644 index 0000000000000..8ce420d82a0a4 --- /dev/null +++ b/x-pack/packages/ml/is_populated_object/jest.config.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../..', + roots: ['/x-pack/packages/ml/is_populated_object'], +}; diff --git a/x-pack/packages/ml/is_populated_object/package.json b/x-pack/packages/ml/is_populated_object/package.json new file mode 100644 index 0000000000000..3ca3e0fcffb01 --- /dev/null +++ b/x-pack/packages/ml/is_populated_object/package.json @@ -0,0 +1,13 @@ +{ + "name": "@kbn/ml-is-populated-object", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0", + "devDependencies": { + "ts-readme": "^1.1.3" + }, + "scripts": { + "generate-docs": "ts-readme src/index.ts" + } +} diff --git a/x-pack/packages/ml/is_populated_object/src/index.ts b/x-pack/packages/ml/is_populated_object/src/index.ts new file mode 100644 index 0000000000000..b2b1532739628 --- /dev/null +++ b/x-pack/packages/ml/is_populated_object/src/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 { isPopulatedObject } from './is_populated_object'; diff --git a/x-pack/packages/ml/is_populated_object/src/is_populated_object.test.ts b/x-pack/packages/ml/is_populated_object/src/is_populated_object.test.ts new file mode 100644 index 0000000000000..c606c0677cf67 --- /dev/null +++ b/x-pack/packages/ml/is_populated_object/src/is_populated_object.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isPopulatedObject } from './is_populated_object'; + +describe('isPopulatedObject', () => { + it('does not allow numbers', () => { + expect(isPopulatedObject(0)).toBe(false); + }); + it('does not allow strings', () => { + expect(isPopulatedObject('')).toBe(false); + }); + it('does not allow null', () => { + expect(isPopulatedObject(null)).toBe(false); + }); + it('does not allow an empty object', () => { + expect(isPopulatedObject({})).toBe(false); + }); + it('allows an object with an attribute', () => { + expect(isPopulatedObject({ attribute: 'value' })).toBe(true); + }); + it('does not allow an object with a non-existing required attribute', () => { + expect(isPopulatedObject({ attribute: 'value' }, ['otherAttribute'])).toBe(false); + }); + it('allows an object with an existing required attribute', () => { + expect(isPopulatedObject({ attribute: 'value' }, ['attribute'])).toBe(true); + }); + it('allows an object with two existing required attributes', () => { + expect( + isPopulatedObject({ attribute1: 'value1', attribute2: 'value2' }, [ + 'attribute1', + 'attribute2', + ]) + ).toBe(true); + }); + it('does not allow an object with two required attributes where one does not exist', () => { + expect( + isPopulatedObject({ attribute1: 'value1', attribute2: 'value2' }, [ + 'attribute1', + 'otherAttribute', + ]) + ).toBe(false); + }); +}); diff --git a/x-pack/plugins/data_visualizer/common/utils/object_utils.ts b/x-pack/packages/ml/is_populated_object/src/is_populated_object.ts similarity index 99% rename from x-pack/plugins/data_visualizer/common/utils/object_utils.ts rename to x-pack/packages/ml/is_populated_object/src/is_populated_object.ts index 537ee9202b4de..43c529206bb63 100644 --- a/x-pack/plugins/data_visualizer/common/utils/object_utils.ts +++ b/x-pack/packages/ml/is_populated_object/src/is_populated_object.ts @@ -5,7 +5,7 @@ * 2.0. */ -/* +/** * A type guard to check record like object structures. * * Examples: diff --git a/x-pack/packages/ml/is_populated_object/tsconfig.json b/x-pack/packages/ml/is_populated_object/tsconfig.json new file mode 100644 index 0000000000000..97a3644c3c703 --- /dev/null +++ b/x-pack/packages/ml/is_populated_object/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/x-pack/packages/ml/is_populated_object/yarn.lock b/x-pack/packages/ml/is_populated_object/yarn.lock new file mode 100644 index 0000000000000..e826cf14c9da2 --- /dev/null +++ b/x-pack/packages/ml/is_populated_object/yarn.lock @@ -0,0 +1,300 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@types/command-line-args@^5.0.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@types/command-line-args/-/command-line-args-5.2.0.tgz#adbb77980a1cc376bb208e3f4142e907410430f6" + integrity sha512-UuKzKpJJ/Ief6ufIaIzr3A/0XnluX7RvFgwkV89Yzvm77wCh1kFaFmqN8XEnGcN62EuHdedQjEMb8mYxFLGPyA== + +"@types/command-line-usage@^5.0.1": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@types/command-line-usage/-/command-line-usage-5.0.2.tgz#ba5e3f6ae5a2009d466679cc431b50635bf1a064" + integrity sha512-n7RlEEJ+4x4TS7ZQddTmNSxP+zziEG0TNsMfiRIxcIVXt71ENJ9ojeXmGO3wPoTdn7pJcU2xc3CJYMktNT6DPg== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +array-back@^3.0.1, array-back@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" + integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== + +array-back@^4.0.1, array-back@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e" + integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== + +braces@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +command-line-application@^0.9.6: + version "0.9.6" + resolved "https://registry.yarnpkg.com/command-line-application/-/command-line-application-0.9.6.tgz#03da3db29a0dbee1af601f03198a2f2425d67803" + integrity sha512-7wc7YX7s/hqZWKp4r37IBlW/Bhh92HWeQW2VV++Mt9x35AKFntz9f7A94Zz+AsImHZmRGHd8iNW5m0jUd4GQpg== + dependencies: + "@types/command-line-args" "^5.0.0" + "@types/command-line-usage" "^5.0.1" + chalk "^2.4.1" + command-line-args "^5.1.1" + command-line-usage "^6.0.0" + meant "^1.0.1" + remove-markdown "^0.3.0" + tslib "1.10.0" + +command-line-args@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e" + integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg== + dependencies: + array-back "^3.1.0" + find-replace "^3.0.0" + lodash.camelcase "^4.3.0" + typical "^4.0.0" + +command-line-usage@^6.0.0: + version "6.1.3" + resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.3.tgz#428fa5acde6a838779dfa30e44686f4b6761d957" + integrity sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw== + dependencies: + array-back "^4.0.2" + chalk "^2.4.2" + table-layout "^1.0.2" + typical "^5.2.0" + +deep-extend@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +fast-glob@^3.1.1: + version "3.2.11" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" + integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fastq@^1.6.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + dependencies: + reusify "^1.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-replace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" + integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== + dependencies: + array-back "^3.0.1" + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + +meant@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/meant/-/meant-1.0.3.tgz#67769af9de1d158773e928ae82c456114903554c" + integrity sha512-88ZRGcNxAq4EH38cQ4D85PM57pikCwS8Z99EWHODxN7KBY+UuPiqzRTtZzS8KTXO/ywSWbdjjJST2Hly/EQxLw== + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +prettier@1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" + integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +reduce-flatten@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" + integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== + +remove-markdown@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/remove-markdown/-/remove-markdown-0.3.0.tgz#5e4b667493a93579728f3d52ecc1db9ca505dc98" + integrity sha512-5392eIuy1mhjM74739VunOlsOYKjsH82rQcTBlJ1bkICVC3dQ3ksQzTHh4jGHQFnM+1xzLzcFOMH+BofqXhroQ== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +table-layout@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.2.tgz#c4038a1853b0136d63365a734b6931cf4fad4a04" + integrity sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A== + dependencies: + array-back "^4.0.1" + deep-extend "~0.6.0" + typical "^5.2.0" + wordwrapjs "^4.0.0" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +ts-readme@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/ts-readme/-/ts-readme-1.1.3.tgz#18a73d21f3bb50ee8e2df819bcbbe3a76385b15a" + integrity sha512-GvI+Vu3m/LGBlgrWwzSmvslnz8msJLNrZ7hQ3Ko2B6PMxeXidqsn6fi20IWgepFjOzhKGw/WlG8NmM7jl3DWeg== + dependencies: + command-line-application "^0.9.6" + fast-glob "^3.1.1" + prettier "1.19.1" + +tslib@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + +typical@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" + integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== + +typical@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066" + integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg== + +wordwrapjs@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.1.tgz#d9790bccfb110a0fc7836b5ebce0937b37a8b98f" + integrity sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA== + dependencies: + reduce-flatten "^2.0.0" + typical "^5.2.0" diff --git a/x-pack/packages/ml/string_hash/BUILD.bazel b/x-pack/packages/ml/string_hash/BUILD.bazel new file mode 100644 index 0000000000000..50e89a8975b51 --- /dev/null +++ b/x-pack/packages/ml/string_hash/BUILD.bazel @@ -0,0 +1,114 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "string_hash" +PKG_REQUIRE_NAME = "@kbn/ml-string-hash" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/x-pack/packages/ml/string_hash/README.md b/x-pack/packages/ml/string_hash/README.md new file mode 100644 index 0000000000000..32ea547e20f37 --- /dev/null +++ b/x-pack/packages/ml/string_hash/README.md @@ -0,0 +1,15 @@ +# @kbn/ml-string-hash + + + +### `stringHash` (function) + +Creates a deterministic number based hash out of a string. + +**Parameters:** + +- str (`string`) + +**returns:** number + + diff --git a/x-pack/packages/ml/string_hash/jest.config.js b/x-pack/packages/ml/string_hash/jest.config.js new file mode 100644 index 0000000000000..4f9fe0d1c70e0 --- /dev/null +++ b/x-pack/packages/ml/string_hash/jest.config.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../..', + roots: ['/x-pack/packages/ml/string_hash'], +}; diff --git a/x-pack/packages/ml/string_hash/package.json b/x-pack/packages/ml/string_hash/package.json new file mode 100644 index 0000000000000..81a0bf1c743f5 --- /dev/null +++ b/x-pack/packages/ml/string_hash/package.json @@ -0,0 +1,13 @@ +{ + "name": "@kbn/ml-string-hash", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0", + "devDependencies": { + "ts-readme": "^1.1.3" + }, + "scripts": { + "generate-docs": "ts-readme src/index.ts" + } +} diff --git a/x-pack/packages/ml/string_hash/src/index.ts b/x-pack/packages/ml/string_hash/src/index.ts new file mode 100644 index 0000000000000..f833c95914fb2 --- /dev/null +++ b/x-pack/packages/ml/string_hash/src/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 { stringHash } from './string_hash'; diff --git a/x-pack/packages/ml/string_hash/src/string_hash.test.ts b/x-pack/packages/ml/string_hash/src/string_hash.test.ts new file mode 100644 index 0000000000000..3354e36455790 --- /dev/null +++ b/x-pack/packages/ml/string_hash/src/string_hash.test.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { stringHash } from './string_hash'; + +describe('stringHash', () => { + test('should return a unique number based off a string', () => { + const hash1 = stringHash('the-string-1'); + const hash2 = stringHash('the-string-2'); + expect(hash1).not.toBe(hash2); + }); +}); diff --git a/x-pack/plugins/data_visualizer/common/utils/string_utils.ts b/x-pack/packages/ml/string_hash/src/string_hash.ts similarity index 100% rename from x-pack/plugins/data_visualizer/common/utils/string_utils.ts rename to x-pack/packages/ml/string_hash/src/string_hash.ts diff --git a/x-pack/packages/ml/string_hash/tsconfig.json b/x-pack/packages/ml/string_hash/tsconfig.json new file mode 100644 index 0000000000000..97a3644c3c703 --- /dev/null +++ b/x-pack/packages/ml/string_hash/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/x-pack/packages/ml/string_hash/yarn.lock b/x-pack/packages/ml/string_hash/yarn.lock new file mode 100644 index 0000000000000..e826cf14c9da2 --- /dev/null +++ b/x-pack/packages/ml/string_hash/yarn.lock @@ -0,0 +1,300 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@types/command-line-args@^5.0.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@types/command-line-args/-/command-line-args-5.2.0.tgz#adbb77980a1cc376bb208e3f4142e907410430f6" + integrity sha512-UuKzKpJJ/Ief6ufIaIzr3A/0XnluX7RvFgwkV89Yzvm77wCh1kFaFmqN8XEnGcN62EuHdedQjEMb8mYxFLGPyA== + +"@types/command-line-usage@^5.0.1": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@types/command-line-usage/-/command-line-usage-5.0.2.tgz#ba5e3f6ae5a2009d466679cc431b50635bf1a064" + integrity sha512-n7RlEEJ+4x4TS7ZQddTmNSxP+zziEG0TNsMfiRIxcIVXt71ENJ9ojeXmGO3wPoTdn7pJcU2xc3CJYMktNT6DPg== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +array-back@^3.0.1, array-back@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" + integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== + +array-back@^4.0.1, array-back@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e" + integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== + +braces@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +command-line-application@^0.9.6: + version "0.9.6" + resolved "https://registry.yarnpkg.com/command-line-application/-/command-line-application-0.9.6.tgz#03da3db29a0dbee1af601f03198a2f2425d67803" + integrity sha512-7wc7YX7s/hqZWKp4r37IBlW/Bhh92HWeQW2VV++Mt9x35AKFntz9f7A94Zz+AsImHZmRGHd8iNW5m0jUd4GQpg== + dependencies: + "@types/command-line-args" "^5.0.0" + "@types/command-line-usage" "^5.0.1" + chalk "^2.4.1" + command-line-args "^5.1.1" + command-line-usage "^6.0.0" + meant "^1.0.1" + remove-markdown "^0.3.0" + tslib "1.10.0" + +command-line-args@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e" + integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg== + dependencies: + array-back "^3.1.0" + find-replace "^3.0.0" + lodash.camelcase "^4.3.0" + typical "^4.0.0" + +command-line-usage@^6.0.0: + version "6.1.3" + resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.3.tgz#428fa5acde6a838779dfa30e44686f4b6761d957" + integrity sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw== + dependencies: + array-back "^4.0.2" + chalk "^2.4.2" + table-layout "^1.0.2" + typical "^5.2.0" + +deep-extend@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +fast-glob@^3.1.1: + version "3.2.11" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" + integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fastq@^1.6.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + dependencies: + reusify "^1.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-replace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" + integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== + dependencies: + array-back "^3.0.1" + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + +meant@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/meant/-/meant-1.0.3.tgz#67769af9de1d158773e928ae82c456114903554c" + integrity sha512-88ZRGcNxAq4EH38cQ4D85PM57pikCwS8Z99EWHODxN7KBY+UuPiqzRTtZzS8KTXO/ywSWbdjjJST2Hly/EQxLw== + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +prettier@1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" + integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +reduce-flatten@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" + integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== + +remove-markdown@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/remove-markdown/-/remove-markdown-0.3.0.tgz#5e4b667493a93579728f3d52ecc1db9ca505dc98" + integrity sha512-5392eIuy1mhjM74739VunOlsOYKjsH82rQcTBlJ1bkICVC3dQ3ksQzTHh4jGHQFnM+1xzLzcFOMH+BofqXhroQ== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +table-layout@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.2.tgz#c4038a1853b0136d63365a734b6931cf4fad4a04" + integrity sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A== + dependencies: + array-back "^4.0.1" + deep-extend "~0.6.0" + typical "^5.2.0" + wordwrapjs "^4.0.0" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +ts-readme@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/ts-readme/-/ts-readme-1.1.3.tgz#18a73d21f3bb50ee8e2df819bcbbe3a76385b15a" + integrity sha512-GvI+Vu3m/LGBlgrWwzSmvslnz8msJLNrZ7hQ3Ko2B6PMxeXidqsn6fi20IWgepFjOzhKGw/WlG8NmM7jl3DWeg== + dependencies: + command-line-application "^0.9.6" + fast-glob "^3.1.1" + prettier "1.19.1" + +tslib@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + +typical@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" + integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== + +typical@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066" + integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg== + +wordwrapjs@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.1.tgz#d9790bccfb110a0fc7836b5ebce0937b37a8b98f" + integrity sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA== + dependencies: + reduce-flatten "^2.0.0" + typical "^5.2.0" diff --git a/x-pack/plugins/data_visualizer/common/types/field_stats.ts b/x-pack/plugins/data_visualizer/common/types/field_stats.ts index 9cc1f7d84f4e2..75fa662281fb3 100644 --- a/x-pack/plugins/data_visualizer/common/types/field_stats.ts +++ b/x-pack/plugins/data_visualizer/common/types/field_stats.ts @@ -8,7 +8,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { Query } from '@kbn/es-query'; import { IKibanaSearchResponse } from '@kbn/data-plugin/common'; -import { isPopulatedObject } from '../utils/object_utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { TimeBucketsInterval } from '../services/time_buckets'; export interface FieldData { diff --git a/x-pack/plugins/data_visualizer/common/types/index.ts b/x-pack/plugins/data_visualizer/common/types/index.ts index 6ab0649bfb9e6..395c9f2f595c2 100644 --- a/x-pack/plugins/data_visualizer/common/types/index.ts +++ b/x-pack/plugins/data_visualizer/common/types/index.ts @@ -6,7 +6,7 @@ */ import type { SimpleSavedObject } from '@kbn/core/public'; -import { isPopulatedObject } from '../utils/object_utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; export type { JobFieldType } from './job_field_type'; export type { FieldRequestConfig, diff --git a/x-pack/plugins/data_visualizer/common/utils/query_utils.ts b/x-pack/plugins/data_visualizer/common/utils/query_utils.ts index dc21bbcae96c3..9f0f746f8909b 100644 --- a/x-pack/plugins/data_visualizer/common/utils/query_utils.ts +++ b/x-pack/plugins/data_visualizer/common/utils/query_utils.ts @@ -40,35 +40,6 @@ export function buildBaseFilterCriteria( return filterCriteria; } -// Wraps the supplied aggregations in a sampler aggregation. -// A supplied samplerShardSize (the shard_size parameter of the sampler aggregation) -// of less than 1 indicates no sampling, and the aggs are returned as-is. -export function buildSamplerAggregation( - aggs: any, - samplerShardSize: number -): Record { - if (samplerShardSize < 1) { - return aggs; - } - - return { - sample: { - sampler: { - shard_size: samplerShardSize, - }, - aggs, - }, - }; -} - -// Returns the path of aggregations in the elasticsearch response, as an array, -// depending on whether sampling is being used. -// A supplied samplerShardSize (the shard_size parameter of the sampler aggregation) -// of less than 1 indicates no sampling, and an empty array is returned. -export function getSamplerAggregationsResponsePath(samplerShardSize: number): string[] { - return samplerShardSize > 0 ? ['sample'] : []; -} - // Returns a name which is safe to use in elasticsearch aggregations for the supplied // field name. Aggregation names must be alpha-numeric and can only contain '_' and '-' characters, // so if the supplied field names contains disallowed characters, the provided index diff --git a/x-pack/plugins/data_visualizer/common/utils/runtime_field_utils.ts b/x-pack/plugins/data_visualizer/common/utils/runtime_field_utils.ts index 6b2cb78d73274..10179d5e51732 100644 --- a/x-pack/plugins/data_visualizer/common/utils/runtime_field_utils.ts +++ b/x-pack/plugins/data_visualizer/common/utils/runtime_field_utils.ts @@ -6,7 +6,7 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { RUNTIME_FIELD_TYPES } from '@kbn/data-plugin/common'; -import { isPopulatedObject } from './object_utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts index c7cfc11c4d630..36b84afadfb04 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts @@ -12,7 +12,7 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { i18n } from '@kbn/i18n'; import type { ToastsStart } from '@kbn/core/public'; import { DataView } from '@kbn/data-views-plugin/public'; -import { isPopulatedObject } from '../../../../../common/utils/object_utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { getTimeFieldRange } from '../../services/time_field_range'; import type { GetTimeFieldRangeResponse } from '../../../../../common/types/time_field_request'; import { addExcludeFrozenToQuery } from '../../utils/query_utils'; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_boolean_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_boolean_field_stats.ts index bceb354295d9c..5b91d3716ffd9 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_boolean_field_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_boolean_field_stats.ts @@ -14,11 +14,9 @@ import type { ISearchOptions, ISearchStart, } from '@kbn/data-plugin/public'; -import { - buildSamplerAggregation, - getSamplerAggregationsResponsePath, -} from '../../../../../common/utils/query_utils'; -import { isPopulatedObject } from '../../../../../common/utils/object_utils'; +import { buildSamplerAggregation, getSamplerAggregationsResponsePath } from '@kbn/ml-agg-utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; + import type { Field, BooleanFieldStats, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_date_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_date_field_stats.ts index 705fe8c002319..1f55f8117c1be 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_date_field_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_date_field_stats.ts @@ -15,11 +15,8 @@ import type { ISearchOptions, ISearchStart, } from '@kbn/data-plugin/public'; -import { - buildSamplerAggregation, - getSamplerAggregationsResponsePath, -} from '../../../../../common/utils/query_utils'; -import { isPopulatedObject } from '../../../../../common/utils/object_utils'; +import { buildSamplerAggregation, getSamplerAggregationsResponsePath } from '@kbn/ml-agg-utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { FieldStatsCommonRequestParams } from '../../../../../common/types/field_stats'; import type { Field, DateFieldStats, Aggs } from '../../../../../common/types/field_stats'; import { FieldStatsError, isIKibanaSearchResponse } from '../../../../../common/types/field_stats'; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_document_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_document_stats.ts index 6cd04de16fa6c..dd654e312e0ef 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_document_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_document_stats.ts @@ -7,8 +7,8 @@ import { each, get } from 'lodash'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { buildBaseFilterCriteria } from '../../../../../common/utils/query_utils'; -import { isPopulatedObject } from '../../../../../common/utils/object_utils'; import type { DocumentCountStats, OverallStatsSearchStrategyParams, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_field_examples.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_field_examples.ts index 8b057caecee7c..0e04665256e20 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_field_examples.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_field_examples.ts @@ -14,8 +14,8 @@ import type { ISearchOptions, ISearchStart, } from '@kbn/data-plugin/public'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { buildBaseFilterCriteria } from '../../../../../common/utils/query_utils'; -import { isPopulatedObject } from '../../../../../common/utils/object_utils'; import type { Field, FieldExamples, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts index 163cb2585f3a6..033f4469b0bc2 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts @@ -16,17 +16,14 @@ import { ISearchOptions, } from '@kbn/data-plugin/common'; import type { ISearchStart } from '@kbn/data-plugin/public'; +import { buildSamplerAggregation, getSamplerAggregationsResponsePath } from '@kbn/ml-agg-utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { MAX_PERCENT, PERCENTILE_SPACING, SAMPLER_TOP_TERMS_SHARD_SIZE, SAMPLER_TOP_TERMS_THRESHOLD, } from './constants'; -import { - buildSamplerAggregation, - getSamplerAggregationsResponsePath, -} from '../../../../../common/utils/query_utils'; -import { isPopulatedObject } from '../../../../../common/utils/object_utils'; import type { Aggs, FieldStatsCommonRequestParams } from '../../../../../common/types/field_stats'; import type { Field, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts index af4777a677bf9..60306ded5d8f4 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts @@ -15,12 +15,9 @@ import type { ISearchOptions, ISearchStart, } from '@kbn/data-plugin/public'; +import { buildSamplerAggregation, getSamplerAggregationsResponsePath } from '@kbn/ml-agg-utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { SAMPLER_TOP_TERMS_SHARD_SIZE, SAMPLER_TOP_TERMS_THRESHOLD } from './constants'; -import { - buildSamplerAggregation, - getSamplerAggregationsResponsePath, -} from '../../../../../common/utils/query_utils'; -import { isPopulatedObject } from '../../../../../common/utils/object_utils'; import type { Aggs, Bucket, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/overall_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/overall_stats.ts index 6a25fac65efe5..a25b3974d45b0 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/overall_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/overall_stats.ts @@ -9,14 +9,13 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { get } from 'lodash'; import { Query } from '@kbn/es-query'; import { IKibanaSearchResponse } from '@kbn/data-plugin/common'; +import { buildSamplerAggregation, getSamplerAggregationsResponsePath } from '@kbn/ml-agg-utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { buildBaseFilterCriteria, - buildSamplerAggregation, getSafeAggregationName, - getSamplerAggregationsResponsePath, } from '../../../../../common/utils/query_utils'; import { getDatafeedAggregations } from '../../../../../common/utils/datafeed_utils'; -import { isPopulatedObject } from '../../../../../common/utils/object_utils'; import { AggregatableField, NonAggregatableField } from '../../types/overall_stats'; import { AggCardinality, Aggs } from '../../../../../common/types/field_stats'; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts index d89a9aca112b3..e8992764a7f97 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts @@ -7,7 +7,7 @@ import { HttpFetchError } from '@kbn/core/public'; import Boom from '@hapi/boom'; -import { isPopulatedObject } from '../../../../common/utils/object_utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; export interface WrappedError { body: { diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.ts index 43c5d49d1986f..5ceda44fa44b3 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.ts @@ -7,7 +7,7 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { cloneDeep } from 'lodash'; -import { isPopulatedObject } from '../../../../common/utils/object_utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; export const addExcludeFrozenToQuery = (originalQuery: QueryDslQueryContainer | undefined) => { const FROZEN_TIER_TERM = { diff --git a/x-pack/plugins/file_upload/common/utils.ts b/x-pack/plugins/file_upload/common/utils.ts deleted file mode 100644 index 5ef6c56392e56..0000000000000 --- a/x-pack/plugins/file_upload/common/utils.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const isPopulatedObject = ( - arg: unknown, - requiredAttributes: U[] = [] -): arg is Record => { - return ( - typeof arg === 'object' && - arg !== null && - Object.keys(arg).length > 0 && - (requiredAttributes.length === 0 || - requiredAttributes.every((d) => ({}.hasOwnProperty.call(arg, d)))) - ); -}; diff --git a/x-pack/plugins/file_upload/public/importer/importer.ts b/x-pack/plugins/file_upload/public/importer/importer.ts index 58809e736720c..8928c4849435f 100644 --- a/x-pack/plugins/file_upload/public/importer/importer.ts +++ b/x-pack/plugins/file_upload/public/importer/importer.ts @@ -8,6 +8,7 @@ import { chunk, intersection } from 'lodash'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { getHttp } from '../kibana_services'; import { MB } from '../../common/constants'; import type { @@ -19,7 +20,6 @@ import type { IngestPipeline, } from '../../common/types'; import { CreateDocsResponse, IImporter, ImportResults } from './types'; -import { isPopulatedObject } from '../../common/utils'; const CHUNK_SIZE = 5000; const REDUCED_CHUNK_SIZE = 100; diff --git a/x-pack/plugins/file_upload/server/get_time_field_range.ts b/x-pack/plugins/file_upload/server/get_time_field_range.ts index e8b21e6807a8f..32bf1766f8d90 100644 --- a/x-pack/plugins/file_upload/server/get_time_field_range.ts +++ b/x-pack/plugins/file_upload/server/get_time_field_range.ts @@ -7,7 +7,7 @@ import { IScopedClusterClient } from '@kbn/core/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; -import { isPopulatedObject } from '../common/utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; export async function getTimeFieldRange( client: IScopedClusterClient, diff --git a/x-pack/plugins/file_upload/server/utils/runtime_field_utils.ts b/x-pack/plugins/file_upload/server/utils/runtime_field_utils.ts index 0daf9713d8263..75c2aa886ef68 100644 --- a/x-pack/plugins/file_upload/server/utils/runtime_field_utils.ts +++ b/x-pack/plugins/file_upload/server/utils/runtime_field_utils.ts @@ -7,7 +7,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { RUNTIME_FIELD_TYPES } from '@kbn/data-plugin/common'; -import { isPopulatedObject } from '../../common/utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; diff --git a/x-pack/plugins/ml/common/index.ts b/x-pack/plugins/ml/common/index.ts index cfed678a804a1..8d419f120a564 100644 --- a/x-pack/plugins/ml/common/index.ts +++ b/x-pack/plugins/ml/common/index.ts @@ -14,7 +14,6 @@ export { SEVERITY_COLORS, } from './constants/anomalies'; export { getSeverityColor, getSeverityType } from './util/anomaly_utils'; -export { isPopulatedObject } from './util/object_utils'; export { composeValidators, patternValidator } from './util/validators'; export { isRuntimeMappings, isRuntimeField } from './util/runtime_field_utils'; export { extractErrorMessage } from './util/errors'; diff --git a/x-pack/plugins/ml/common/types/es_client.ts b/x-pack/plugins/ml/common/types/es_client.ts index 44425619af39d..9d8f5f3dbed9f 100644 --- a/x-pack/plugins/ml/common/types/es_client.ts +++ b/x-pack/plugins/ml/common/types/es_client.ts @@ -6,7 +6,7 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { isPopulatedObject } from '../util/object_utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; export function isMultiBucketAggregate( arg: unknown diff --git a/x-pack/plugins/ml/common/types/feature_importance.ts b/x-pack/plugins/ml/common/types/feature_importance.ts index 111c8432dd439..3333f11ecd2e6 100644 --- a/x-pack/plugins/ml/common/types/feature_importance.ts +++ b/x-pack/plugins/ml/common/types/feature_importance.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { isPopulatedObject } from '../util/object_utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; export type FeatureImportanceClassName = string | number | boolean; diff --git a/x-pack/plugins/ml/common/util/errors/process_errors.ts b/x-pack/plugins/ml/common/util/errors/process_errors.ts index e5c6ed38161ab..0da2650fa5fd6 100644 --- a/x-pack/plugins/ml/common/util/errors/process_errors.ts +++ b/x-pack/plugins/ml/common/util/errors/process_errors.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { ErrorType, MLErrorObject, @@ -14,7 +15,6 @@ import { isEsErrorBody, isMLResponseError, } from './types'; -import { isPopulatedObject } from '../object_utils'; export const extractErrorProperties = (error: ErrorType): MLErrorObject => { // extract properties of the error object from within the response error diff --git a/x-pack/plugins/ml/common/util/group_color_utils.ts b/x-pack/plugins/ml/common/util/group_color_utils.ts index b9709671475be..3c2398a18684f 100644 --- a/x-pack/plugins/ml/common/util/group_color_utils.ts +++ b/x-pack/plugins/ml/common/util/group_color_utils.ts @@ -7,7 +7,7 @@ import { euiDarkVars as euiVars } from '@kbn/ui-theme'; -import { stringHash } from './string_utils'; +import { stringHash } from '@kbn/ml-string-hash'; const COLORS = [ euiVars.euiColorVis0, diff --git a/x-pack/plugins/ml/common/util/job_utils.ts b/x-pack/plugins/ml/common/util/job_utils.ts index f1991118d4e36..d7faf732a7c84 100644 --- a/x-pack/plugins/ml/common/util/job_utils.ts +++ b/x-pack/plugins/ml/common/util/job_utils.ts @@ -9,9 +9,9 @@ import { each, isEmpty, isEqual, pick } from 'lodash'; import semverGte from 'semver/functions/gte'; import moment, { Duration } from 'moment'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -// @ts-ignore import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { ALLOWED_DATA_UNITS, JOB_ID_MAX_LENGTH } from '../constants/validation'; import { parseInterval } from './parse_interval'; import { maxLengthValidator } from './validators'; @@ -24,7 +24,7 @@ import { ES_AGGREGATION, ML_JOB_AGGREGATION } from '../constants/aggregation_typ import { MLCATEGORY } from '../constants/field_types'; import { getAggregations, getDatafeedAggregations } from './datafeed_utils'; import { findAggField } from './validation_utils'; -import { getFirstKeyInObject, isPopulatedObject } from './object_utils'; +import { getFirstKeyInObject } from './object_utils'; import { isDefined } from '../types/guards'; export interface ValidationResults { diff --git a/x-pack/plugins/ml/common/util/object_utils.test.ts b/x-pack/plugins/ml/common/util/object_utils.test.ts index d6d500cdb82c6..e6a0617c6335e 100644 --- a/x-pack/plugins/ml/common/util/object_utils.test.ts +++ b/x-pack/plugins/ml/common/util/object_utils.test.ts @@ -5,49 +5,9 @@ * 2.0. */ -import { getFirstKeyInObject, isPopulatedObject } from './object_utils'; +import { getFirstKeyInObject } from './object_utils'; describe('object_utils', () => { - describe('isPopulatedObject()', () => { - it('does not allow numbers', () => { - expect(isPopulatedObject(0)).toBe(false); - }); - it('does not allow strings', () => { - expect(isPopulatedObject('')).toBe(false); - }); - it('does not allow null', () => { - expect(isPopulatedObject(null)).toBe(false); - }); - it('does not allow an empty object', () => { - expect(isPopulatedObject({})).toBe(false); - }); - it('allows an object with an attribute', () => { - expect(isPopulatedObject({ attribute: 'value' })).toBe(true); - }); - it('does not allow an object with a non-existing required attribute', () => { - expect(isPopulatedObject({ attribute: 'value' }, ['otherAttribute'])).toBe(false); - }); - it('allows an object with an existing required attribute', () => { - expect(isPopulatedObject({ attribute: 'value' }, ['attribute'])).toBe(true); - }); - it('allows an object with two existing required attributes', () => { - expect( - isPopulatedObject({ attribute1: 'value1', attribute2: 'value2' }, [ - 'attribute1', - 'attribute2', - ]) - ).toBe(true); - }); - it('does not allow an object with two required attributes where one does not exist', () => { - expect( - isPopulatedObject({ attribute1: 'value1', attribute2: 'value2' }, [ - 'attribute1', - 'otherAttribute', - ]) - ).toBe(false); - }); - }); - describe('getFirstKeyInObject()', () => { it('gets the first key in object', () => { expect(getFirstKeyInObject({ attribute1: 'value', attribute2: 'value2' })).toBe('attribute1'); diff --git a/x-pack/plugins/ml/common/util/object_utils.ts b/x-pack/plugins/ml/common/util/object_utils.ts index cd62ca006725e..2bf2e301f9473 100644 --- a/x-pack/plugins/ml/common/util/object_utils.ts +++ b/x-pack/plugins/ml/common/util/object_utils.ts @@ -5,35 +5,7 @@ * 2.0. */ -/* - * A type guard to check record like object structures. - * - * Examples: - * - `isPopulatedObject({...})` - * Limits type to Record - * - * - `isPopulatedObject({...}, ['attribute'])` - * Limits type to Record<'attribute', unknown> - * - * - `isPopulatedObject({...})` - * Limits type to a record with keys of the given interface. - * Note that you might want to add keys from the interface to the - * array of requiredAttributes to satisfy runtime requirements. - * Otherwise you'd just satisfy TS requirements but might still - * run into runtime issues. - */ -export const isPopulatedObject = ( - arg: unknown, - requiredAttributes: U[] = [] -): arg is Record => { - return ( - typeof arg === 'object' && - arg !== null && - Object.keys(arg).length > 0 && - (requiredAttributes.length === 0 || - requiredAttributes.every((d) => ({}.hasOwnProperty.call(arg, d)))) - ); -}; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; /** * Get the first key in the object diff --git a/x-pack/plugins/ml/common/util/query_utils.ts b/x-pack/plugins/ml/common/util/query_utils.ts index 22c0f45f2f239..5ceda44fa44b3 100644 --- a/x-pack/plugins/ml/common/util/query_utils.ts +++ b/x-pack/plugins/ml/common/util/query_utils.ts @@ -7,7 +7,7 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { cloneDeep } from 'lodash'; -import { isPopulatedObject } from './object_utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; export const addExcludeFrozenToQuery = (originalQuery: QueryDslQueryContainer | undefined) => { const FROZEN_TIER_TERM = { diff --git a/x-pack/plugins/ml/common/util/runtime_field_utils.ts b/x-pack/plugins/ml/common/util/runtime_field_utils.ts index 6b2cb78d73274..10179d5e51732 100644 --- a/x-pack/plugins/ml/common/util/runtime_field_utils.ts +++ b/x-pack/plugins/ml/common/util/runtime_field_utils.ts @@ -6,7 +6,7 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { RUNTIME_FIELD_TYPES } from '@kbn/data-plugin/common'; -import { isPopulatedObject } from './object_utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; diff --git a/x-pack/plugins/ml/common/util/string_utils.test.ts b/x-pack/plugins/ml/common/util/string_utils.test.ts index 52a3c10da8b5a..43acc80110001 100644 --- a/x-pack/plugins/ml/common/util/string_utils.test.ts +++ b/x-pack/plugins/ml/common/util/string_utils.test.ts @@ -5,12 +5,7 @@ * 2.0. */ -import { - renderTemplate, - getMedianStringLength, - stringHash, - getGroupQueryText, -} from './string_utils'; +import { renderTemplate, getMedianStringLength, getGroupQueryText } from './string_utils'; const strings: string[] = [ 'foo', @@ -53,14 +48,6 @@ describe('ML - string utils', () => { }); }); - describe('stringHash', () => { - test('should return a unique number based off a string', () => { - const hash1 = stringHash('the-string-1'); - const hash2 = stringHash('the-string-2'); - expect(hash1).not.toBe(hash2); - }); - }); - describe('getGroupQueryText', () => { const groupIdOne = 'test_group_id_1'; const groupIdTwo = 'test_group_id_2'; diff --git a/x-pack/plugins/ml/common/util/string_utils.ts b/x-pack/plugins/ml/common/util/string_utils.ts index 044b34d166a87..d55d007ceb225 100644 --- a/x-pack/plugins/ml/common/util/string_utils.ts +++ b/x-pack/plugins/ml/common/util/string_utils.ts @@ -24,23 +24,6 @@ export function getMedianStringLength(strings: string[]) { return sortedStringLengths[Math.floor(sortedStringLengths.length / 2)] || 0; } -/** - * Creates a deterministic number based hash out of a string. - */ -export function stringHash(str: string): number { - let hash = 0; - let chr = 0; - if (str.length === 0) { - return hash; - } - for (let i = 0; i < str.length; i++) { - chr = str.charCodeAt(i); - hash = (hash << 5) - hash + chr; // eslint-disable-line no-bitwise - hash |= 0; // eslint-disable-line no-bitwise - } - return hash < 0 ? hash * -2 : hash; -} - export function getGroupQueryText(groupIds: string[]): string { return `groups:(${groupIds.join(' or ')})`; } diff --git a/x-pack/plugins/ml/common/util/validators.ts b/x-pack/plugins/ml/common/util/validators.ts index 0936efbcb00fc..b28e120b1f196 100644 --- a/x-pack/plugins/ml/common/util/validators.ts +++ b/x-pack/plugins/ml/common/util/validators.ts @@ -5,9 +5,9 @@ * 2.0. */ +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { ALLOWED_DATA_UNITS } from '../constants/validation'; import { parseInterval } from './parse_interval'; -import { isPopulatedObject } from './object_utils'; /** * Provides a validator function for maximum allowed input length. diff --git a/x-pack/plugins/ml/public/alerting/jobs_health_rule/anomaly_detection_jobs_health_rule_trigger.tsx b/x-pack/plugins/ml/public/alerting/jobs_health_rule/anomaly_detection_jobs_health_rule_trigger.tsx index dcbf580461e0f..1b88b38788554 100644 --- a/x-pack/plugins/ml/public/alerting/jobs_health_rule/anomaly_detection_jobs_health_rule_trigger.tsx +++ b/x-pack/plugins/ml/public/alerting/jobs_health_rule/anomaly_detection_jobs_health_rule_trigger.tsx @@ -11,13 +11,13 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import useDebounce from 'react-use/lib/useDebounce'; import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { MlAnomalyDetectionJobsHealthRuleParams } from '../../../common/types/alerts'; import { JobSelectorControl } from '../job_selector'; import { jobsApiProvider } from '../../application/services/ml_api_service/jobs'; import { HttpService } from '../../application/services/http_service'; import { useMlKibana } from '../../application/contexts/kibana'; import { TestsSelectionControl } from './tests_selection_control'; -import { isPopulatedObject } from '../../../common'; import { ALL_JOBS_SELECTION } from '../../../common/constants/alerts'; import { BetaBadge } from '../beta_badge'; import { isDefined } from '../../../common/types/guards'; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx index 82ae725038387..3edc757c11846 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx @@ -27,6 +27,7 @@ import { } from '@elastic/eui'; import { CoreSetup } from '@kbn/core/public'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { DEFAULT_SAMPLER_SHARD_SIZE } from '../../../../common/constants/field_histograms'; import { ANALYSIS_CONFIG_TYPE, INDEX_STATUS } from '../../data_frame_analytics/common'; @@ -44,7 +45,6 @@ import { FeatureImportance, TopClasses, } from '../../../../common/types/feature_importance'; -import { isPopulatedObject } from '../../../../common/util/object_utils'; import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants/data_frame_analytics'; import { DataFrameAnalysisConfigType } from '../../../../common/types/data_frame_analytics'; diff --git a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts index c83836164221d..edaa2c30662b1 100644 --- a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts +++ b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts @@ -11,9 +11,9 @@ import { i18n } from '@kbn/i18n'; import dateMath from '@kbn/datemath'; import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import type { DataView } from '@kbn/data-views-plugin/public'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { getTimefilter, getToastNotifications } from '../../util/dependency_cache'; import { ml, GetTimeFieldRangeResponse } from '../../services/ml_api_service'; -import { isPopulatedObject } from '../../../../common/util/object_utils'; import type { RuntimeMappings } from '../../../../common/types/fields'; import { addExcludeFrozenToQuery } from '../../../../common/util/query_utils'; diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx index b93ba79830f98..e11de5fea47d3 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx @@ -25,9 +25,9 @@ import { import { i18n } from '@kbn/i18n'; import { DataView } from '@kbn/data-views-plugin/public'; +import { stringHash } from '@kbn/ml-string-hash'; import { extractErrorMessage } from '../../../../common'; import { isRuntimeMappings } from '../../../../common/util/runtime_field_utils'; -import { stringHash } from '../../../../common/util/string_utils'; import { RuntimeMappings } from '../../../../common/types/fields'; import type { ResultsSearchQuery } from '../../data_frame_analytics/common/analytics'; import { getCombinedRuntimeMappings } from '../data_grid'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx index 9de32fdd5bd3e..962a06410ca50 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx @@ -21,10 +21,10 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { XJsonMode } from '@kbn/ace'; import { XJson } from '@kbn/es-ui-shared-plugin/public'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { useMlContext } from '../../../../../contexts/ml'; import { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/use_create_analytics_form'; import { getCombinedRuntimeMappings } from '../../../../../components/data_grid/common'; -import { isPopulatedObject } from '../../../../../../../common/util/object_utils'; import { RuntimeMappingsEditor } from './runtime_mappings_editor'; import { isRuntimeMappings } from '../../../../../../../common'; import { SwitchModal } from './switch_modal'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_exploration_url_state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_exploration_url_state.ts index dac1a85211c20..1a26cce465d85 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_exploration_url_state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_exploration_url_state.ts @@ -5,11 +5,11 @@ * 2.0. */ +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { usePageUrlState } from '../../../../util/url_state'; import { ML_PAGES } from '../../../../../../common/constants/locator'; import { ExplorationPageUrlState } from '../../../../../../common/types/locator'; import { SEARCH_QUERY_LANGUAGE } from '../../../../../../common/constants/search'; -import { isPopulatedObject } from '../../../../../../common/util/object_utils'; export function getDefaultExplorationPageUrlState( overrides?: Partial diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js index d6926950dce7d..364cdd1be55db 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -21,7 +21,7 @@ import { JOB_STATE, DATAFEED_STATE } from '../../../../../common/constants/state import { JOB_ACTION } from '../../../../../common/constants/job_actions'; import { parseInterval } from '../../../../../common/util/parse_interval'; import { mlCalendarService } from '../../../services/calendar_service'; -import { isPopulatedObject } from '../../../../../common/util/object_utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; export function loadFullJob(jobId) { return new Promise((resolve, reject) => { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_utils.ts b/x-pack/plugins/ml/public/application/jobs/jobs_utils.ts index 103f079b88ff3..79952312723fa 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_utils.ts +++ b/x-pack/plugins/ml/public/application/jobs/jobs_utils.ts @@ -6,7 +6,7 @@ */ import { MlJob } from '@elastic/elasticsearch/lib/api/types'; -import { isPopulatedObject } from '../../../common'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { MlSummaryJob } from '../../../common/types/anomaly_detection_jobs'; export const isManagedJob = (job: MlSummaryJob | MlJob) => { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.ts index 9f6f301891741..2f081369ca686 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.ts @@ -5,9 +5,9 @@ * 2.0. */ +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { RuntimeMappings } from '../../../../../../../common/types/fields'; import type { Datafeed, Job } from '../../../../../../../common/types/anomaly_detection_jobs'; -import { isPopulatedObject } from '../../../../../../../common/util/object_utils'; interface Response { runtime_mappings: RuntimeMappings; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx index c370778b178c8..59176d06d92c0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx @@ -19,6 +19,7 @@ import { } from '@elastic/eui'; import { merge } from 'lodash'; import moment from 'moment'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { useMlKibana, useMlLocator } from '../../../contexts/kibana'; import { ml } from '../../../services/ml_api_service'; import { useMlContext } from '../../../contexts/ml'; @@ -40,7 +41,6 @@ import { JobId } from '../../../../../common/types/anomaly_detection_jobs'; import { ML_PAGES } from '../../../../../common/constants/locator'; import { TIME_FORMAT } from '../../../../../common/constants/time_format'; import { JobsAwaitingNodeWarning } from '../../../components/jobs_awaiting_node_warning'; -import { isPopulatedObject } from '../../../../../common/util/object_utils'; import { RuntimeMappings } from '../../../../../common/types/fields'; import { addExcludeFrozenToQuery } from '../../../../../common/util/query_utils'; import { MlPageHeader } from '../../../components/page_header'; diff --git a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts index 0ed3b511b669e..d1e42dd72ddaf 100644 --- a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts +++ b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts @@ -9,12 +9,12 @@ import { Observable, of } from 'rxjs'; import { map as mapObservable } from 'rxjs/operators'; import type { TimeRange } from '@kbn/es-query'; import type { TimefilterContract } from '@kbn/data-plugin/public'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { RecordForInfluencer } from './results_service/results_service'; import type { EntityField } from '../../../common/util/anomaly_utils'; import type { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import type { MlApiServices } from './ml_api_service'; import type { MlResultsService } from './results_service'; -import { isPopulatedObject } from '../../../common/util/object_utils'; import { ExplorerChartsData } from '../explorer/explorer_charts/explorer_charts_container_service'; import type { TimeRangeBounds } from '../util/time_buckets'; import { isDefined } from '../../../common/types/guards'; diff --git a/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts b/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts index cbe4017a02835..38bf80be1ffed 100644 --- a/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts +++ b/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts @@ -8,6 +8,7 @@ import { IUiSettingsClient } from '@kbn/core/public'; import type { TimeRange } from '@kbn/es-query'; import { TimefilterContract, UI_SETTINGS } from '@kbn/data-plugin/public'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { getBoundsRoundedToInterval, TimeBuckets, @@ -24,7 +25,6 @@ import { OVERALL_LABEL, VIEW_BY_JOB_LABEL } from '../explorer/explorer_constants import { MlResultsService } from './results_service'; import { EntityField } from '../../../common/util/anomaly_utils'; import { InfluencersFilterQuery } from '../../../common/types/es_client'; -import { isPopulatedObject } from '../../../common'; /** * Service for retrieving anomaly swim lanes data. diff --git a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts index 54dece80f22cf..d5da658755943 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts @@ -15,6 +15,7 @@ import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { each, get } from 'lodash'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { Dictionary } from '../../../../common/types/common'; import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; import { Datafeed, JobId } from '../../../../common/types/anomaly_detection_jobs'; @@ -24,7 +25,6 @@ import { findAggField } from '../../../../common/util/validation_utils'; import { getDatafeedAggregations } from '../../../../common/util/datafeed_utils'; import { aggregationTypeTransform, EntityField } from '../../../../common/util/anomaly_utils'; import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; -import { isPopulatedObject } from '../../../../common/util/object_utils'; import { InfluencersFilterQuery } from '../../../../common/types/es_client'; import { RecordForInfluencer } from './results_service'; import { isRuntimeMappings } from '../../../../common'; diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js index bb6f6b5969ac4..b26e52f5f32f2 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js @@ -7,6 +7,8 @@ import { each, get } from 'lodash'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; + import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; import { escapeForElasticsearchQuery } from '../../util/string_utils'; import { @@ -14,7 +16,6 @@ import { SWIM_LANE_DEFAULT_PAGE_SIZE, } from '../../explorer/explorer_constants'; import { aggregationTypeTransform } from '../../../../common/util/anomaly_utils'; -import { isPopulatedObject } from '../../../../common/util/object_utils'; /** * Service for carrying out Elasticsearch queries to obtain data for the Ml Results dashboards. diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/expanded_row.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/expanded_row.tsx index 8b868a088499d..6ab9261213d56 100644 --- a/x-pack/plugins/ml/public/application/trained_models/models_management/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/expanded_row.tsx @@ -23,9 +23,9 @@ import { import type { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list'; import { FormattedMessage } from '@kbn/i18n-react'; import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { ModelItemFull } from './models_list'; import { isDefined } from '../../../../common/types/guards'; -import { isPopulatedObject } from '../../../../common'; import { ModelPipelines } from './pipelines'; import { AllocatedModels } from '../nodes_overview/allocated_models'; import type { AllocatedModel } from '../../../../common/types/trained_models'; diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx index 4f3cef2eeda12..07b1e876d4120 100644 --- a/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx @@ -26,6 +26,7 @@ import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/bas import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; import { Action } from '@elastic/eui/src/components/basic_table/action_types'; import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { getAnalysisType } from '../../data_frame_analytics/common'; import { ModelsTableToConfigMapping } from '.'; import { ModelsBarStats, StatsBar } from '../../components/stats_bar'; @@ -44,7 +45,6 @@ import { ML_PAGES } from '../../../../common/constants/locator'; import { ListingPageUrlState } from '../../../../common/types/common'; import { usePageUrlState } from '../../util/url_state'; import { ExpandedRow } from './expanded_row'; -import { isPopulatedObject } from '../../../../common'; import { useTableSettings } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings'; import { useToastNotificationService } from '../../services/toast_notification_service'; import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter'; diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/utils.ts b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/utils.ts index 3ac6ec77f576a..e340170ce46ce 100644 --- a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/utils.ts +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/utils.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { TRAINED_MODEL_TYPE, DEPLOYMENT_STATE, @@ -13,8 +14,6 @@ import { import type { SupportedPytorchTasksType } from '../../../../../common/constants/trained_models'; import type { ModelItem } from '../models_list'; -import { isPopulatedObject } from '../../../../../common'; - const PYTORCH_TYPES = Object.values(SUPPORTED_PYTORCH_TASKS); export function isTestable(modelItem: ModelItem) { diff --git a/x-pack/plugins/ml/public/application/util/url_state.tsx b/x-pack/plugins/ml/public/application/util/url_state.tsx index 42d5c012b9c14..a31b574681827 100644 --- a/x-pack/plugins/ml/public/application/util/url_state.tsx +++ b/x-pack/plugins/ml/public/application/util/url_state.tsx @@ -21,11 +21,11 @@ import { useHistory, useLocation } from 'react-router-dom'; import { BehaviorSubject, Observable } from 'rxjs'; import { distinctUntilChanged } from 'rxjs/operators'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { Dictionary } from '../../../common/types/common'; import { getNestedProperty } from './object_utils'; import { MlPages } from '../../../common/constants/locator'; -import { isPopulatedObject } from '../../../common'; type Accessor = '_a' | '_g'; export type SetUrlState = ( diff --git a/x-pack/plugins/ml/public/embeddables/types.ts b/x-pack/plugins/ml/public/embeddables/types.ts index 2ba4af15f7466..6a990db33398a 100644 --- a/x-pack/plugins/ml/public/embeddables/types.ts +++ b/x-pack/plugins/ml/public/embeddables/types.ts @@ -10,6 +10,7 @@ import type { Filter, Query, TimeRange } from '@kbn/es-query'; import type { RefreshInterval } from '@kbn/data-plugin/common'; import type { EmbeddableInput, EmbeddableOutput, IEmbeddable } from '@kbn/embeddable-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { JobId } from '../../common/types/anomaly_detection_jobs'; import type { SwimlaneType } from '../application/explorer/explorer_constants'; import type { AnomalyDetectorService } from '../application/services/anomaly_detector_service'; @@ -18,7 +19,6 @@ import type { MlDependencies } from '../application/app'; import type { AppStateSelectedCells } from '../application/explorer/explorer_utils'; import { AnomalyExplorerChartsService } from '../application/services/anomaly_explorer_charts_service'; import { EntityField } from '../../common/util/anomaly_utils'; -import { isPopulatedObject } from '../../common/util/object_utils'; import { ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE, ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, diff --git a/x-pack/plugins/ml/server/lib/query_utils.test.ts b/x-pack/plugins/ml/server/lib/query_utils.test.ts index a83265ed1262f..c505a8a9b1fa8 100644 --- a/x-pack/plugins/ml/server/lib/query_utils.test.ts +++ b/x-pack/plugins/ml/server/lib/query_utils.test.ts @@ -5,11 +5,7 @@ * 2.0. */ -import { - buildBaseFilterCriteria, - buildSamplerAggregation, - getSamplerAggregationsResponsePath, -} from './query_utils'; +import { buildBaseFilterCriteria } from './query_utils'; describe('ML - query utils', () => { describe('buildBaseFilterCriteria', () => { @@ -52,37 +48,4 @@ describe('ML - query utils', () => { ]); }); }); - - describe('buildSamplerAggregation', () => { - const testAggs = { - bytes_stats: { - stats: { field: 'bytes' }, - }, - }; - - test('returns wrapped sampler aggregation for sampler shard size of 1000', () => { - expect(buildSamplerAggregation(testAggs, 1000)).toEqual({ - sample: { - sampler: { - shard_size: 1000, - }, - aggs: testAggs, - }, - }); - }); - - test('returns un-sampled aggregation as-is for sampler shard size of 0', () => { - expect(buildSamplerAggregation(testAggs, 0)).toEqual(testAggs); - }); - }); - - describe('getSamplerAggregationsResponsePath', () => { - test('returns correct path for sampler shard size of 1000', () => { - expect(getSamplerAggregationsResponsePath(1000)).toEqual(['sample']); - }); - - test('returns correct path for sampler shard size of 0', () => { - expect(getSamplerAggregationsResponsePath(0)).toEqual([]); - }); - }); }); diff --git a/x-pack/plugins/ml/server/lib/query_utils.ts b/x-pack/plugins/ml/server/lib/query_utils.ts index cfaa5abaf7f23..a60622583781b 100644 --- a/x-pack/plugins/ml/server/lib/query_utils.ts +++ b/x-pack/plugins/ml/server/lib/query_utils.ts @@ -5,7 +5,6 @@ * 2.0. */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; /* * Contains utility functions for building and processing queries. */ @@ -37,32 +36,3 @@ export function buildBaseFilterCriteria( return filterCriteria; } - -// Wraps the supplied aggregations in a sampler aggregation. -// A supplied samplerShardSize (the shard_size parameter of the sampler aggregation) -// of less than 1 indicates no sampling, and the aggs are returned as-is. -export function buildSamplerAggregation( - aggs: any, - samplerShardSize: number -): Record { - if (samplerShardSize < 1) { - return aggs; - } - - return { - sample: { - sampler: { - shard_size: samplerShardSize, - }, - aggs, - }, - }; -} - -// Returns the path of aggregations in the elasticsearch response, as an array, -// depending on whether sampling is being used. -// A supplied samplerShardSize (the shard_size parameter of the sampler aggregation) -// of less than 1 indicates no sampling, and an empty array is returned. -export function getSamplerAggregationsResponsePath(samplerShardSize: number): string[] { - return samplerShardSize > 0 ? ['sample'] : []; -} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 9bf107106a056..7060d935b0fef 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -17,6 +17,7 @@ import type { import moment from 'moment'; import { merge } from 'lodash'; import type { DataViewsService } from '@kbn/data-views-plugin/common'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { AnalysisLimits } from '../../../common/types/anomaly_detection_jobs'; import { getAuthorizationHeader } from '../../lib/request_authorization'; import type { MlClient } from '../../lib/ml_client'; @@ -54,7 +55,6 @@ import type { JobExistResult, JobStat } from '../../../common/types/data_recogni import type { Datafeed } from '../../../common/types/anomaly_detection_jobs'; import type { MLSavedObjectService } from '../../saved_objects'; import { isDefined } from '../../../common/types/guards'; -import { isPopulatedObject } from '../../../common/util/object_utils'; const ML_DIR = 'ml'; const KIBANA_DIR = 'kibana'; diff --git a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts index bbfc43257caf5..6946a2fbda90f 100644 --- a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts @@ -5,21 +5,23 @@ * 2.0. */ -import { IScopedClusterClient } from '@kbn/core/server'; import { get, each, last, find } from 'lodash'; + +import { IScopedClusterClient } from '@kbn/core/server'; import { KBN_FIELD_TYPES } from '@kbn/data-plugin/server'; -import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types'; -import { getSafeAggregationName } from '../../../common/util/job_utils'; -import { stringHash } from '../../../common/util/string_utils'; import { - buildBaseFilterCriteria, buildSamplerAggregation, + getAggIntervals, getSamplerAggregationsResponsePath, -} from '../../lib/query_utils'; +} from '@kbn/ml-agg-utils'; +import { stringHash } from '@kbn/ml-string-hash'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types'; +import { getSafeAggregationName } from '../../../common/util/job_utils'; +import { buildBaseFilterCriteria } from '../../lib/query_utils'; import { AggCardinality, RuntimeMappings } from '../../../common/types/fields'; import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; import { Datafeed } from '../../../common/types/anomaly_detection_jobs'; -import { isPopulatedObject } from '../../../common/util/object_utils'; const SAMPLER_TOP_TERMS_THRESHOLD = 100000; const SAMPLER_TOP_TERMS_SHARD_SIZE = 5000; @@ -112,13 +114,6 @@ interface FieldExamples { examples: any[]; } -interface NumericColumnStats { - interval: number; - min: number; - max: number; -} -type NumericColumnStatsMap = Record; - interface AggHistogram { histogram: { field: string; @@ -178,67 +173,6 @@ type BatchStats = | DocumentCountStats | FieldExamples; -const getAggIntervals = async ( - { asCurrentUser }: IScopedClusterClient, - indexPattern: string, - query: any, - fields: HistogramField[], - samplerShardSize: number, - runtimeMappings?: RuntimeMappings -): Promise => { - const numericColumns = fields.filter((field) => { - return field.type === KBN_FIELD_TYPES.NUMBER || field.type === KBN_FIELD_TYPES.DATE; - }); - - if (numericColumns.length === 0) { - return {}; - } - - const minMaxAggs = numericColumns.reduce((aggs, c) => { - const id = stringHash(c.fieldName); - aggs[id] = { - stats: { - field: c.fieldName, - }, - }; - return aggs; - }, {} as Record); - - const body = await asCurrentUser.search( - { - index: indexPattern, - size: 0, - body: { - query, - aggs: buildSamplerAggregation(minMaxAggs, samplerShardSize), - size: 0, - ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), - }, - }, - { maxRetries: 0 } - ); - - const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); - const aggregations = aggsPath.length > 0 ? get(body.aggregations, aggsPath) : body.aggregations; - - return Object.keys(aggregations).reduce((p, aggName) => { - const stats = [aggregations[aggName].min, aggregations[aggName].max]; - if (!stats.includes(null)) { - const delta = aggregations[aggName].max - aggregations[aggName].min; - - let aggInterval = 1; - - if (delta > MAX_CHART_COLUMNS || delta <= 1) { - aggInterval = delta / (MAX_CHART_COLUMNS - 1); - } - - p[aggName] = { interval: aggInterval, min: stats[0], max: stats[1] }; - } - - return p; - }, {} as NumericColumnStatsMap); -}; - // export for re-use by transforms plugin export const getHistogramsForFields = async ( client: IScopedClusterClient, @@ -250,7 +184,7 @@ export const getHistogramsForFields = async ( ) => { const { asCurrentUser } = client; const aggIntervals = await getAggIntervals( - client, + client.asCurrentUser, indexPattern, query, fields, diff --git a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts index 64348a0656009..fd661063bde5f 100644 --- a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts +++ b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts @@ -8,6 +8,7 @@ import Boom from '@hapi/boom'; import { IScopedClusterClient } from '@kbn/core/server'; import { duration } from 'moment'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { parseInterval } from '../../../common/util/parse_interval'; import { initCardinalityFieldsCache } from './fields_aggs_cache'; import { AggCardinality } from '../../../common/types/fields'; @@ -15,7 +16,6 @@ import { isValidAggregationField } from '../../../common/util/validation_utils'; import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; import { Datafeed, IndicesOptions } from '../../../common/types/anomaly_detection_jobs'; import { RuntimeMappings } from '../../../common/types/fields'; -import { isPopulatedObject } from '../../../common/util/object_utils'; /** * Service for carrying out queries to obtain data diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index 1ffa9687fca95..f3cb4c584f27f 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -9,6 +9,7 @@ import { uniq } from 'lodash'; import Boom from '@hapi/boom'; import { IScopedClusterClient } from '@kbn/core/server'; import type { RulesClient } from '@kbn/alerting-plugin/server'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { getSingleMetricViewerJobErrorMessage, parseTimeIntervalForJob, @@ -47,7 +48,6 @@ import { } from '../../../common/util/job_utils'; import { groupsProvider } from './groups'; import type { MlClient } from '../../lib/ml_client'; -import { isPopulatedObject } from '../../../common/util/object_utils'; import { ML_ALERT_TYPES } from '../../../common/constants/alerts'; import { MlAnomalyDetectionAlertParams } from '../../routes/schemas/alerting_schema'; import type { AuthorizationHeader } from '../../lib/request_authorization'; diff --git a/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts b/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts index dd7e2b3373e89..9456e529eb039 100644 --- a/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts +++ b/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts @@ -10,8 +10,9 @@ import { i18n } from '@kbn/i18n'; import { each, find, get, keyBy, map, reduce, sortBy } from 'lodash'; import type * as estypes from '@elastic/elasticsearch/lib/api/types'; import { extent, max, min } from 'd3'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { MlClient } from '../../lib/ml_client'; -import { isPopulatedObject, isRuntimeMappings } from '../../../common'; +import { isRuntimeMappings } from '../../../common'; import type { MetricData, ModelPlotOutput, diff --git a/x-pack/plugins/transform/common/api_schemas/type_guards.ts b/x-pack/plugins/transform/common/api_schemas/type_guards.ts index 567eeb030e22d..821eda892deb3 100644 --- a/x-pack/plugins/transform/common/api_schemas/type_guards.ts +++ b/x-pack/plugins/transform/common/api_schemas/type_guards.ts @@ -7,9 +7,10 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; + import type { EsIndex } from '../types/es_index'; import type { EsIngestPipeline } from '../types/es_ingest_pipeline'; -import { isPopulatedObject } from '../shared_imports'; // To be able to use the type guards on the client side, we need to make sure we don't import // the code of '@kbn/config-schema' but just its types, otherwise the client side code will diff --git a/x-pack/plugins/transform/common/shared_imports.ts b/x-pack/plugins/transform/common/shared_imports.ts index 0566086046d0e..fd00440e07c5b 100644 --- a/x-pack/plugins/transform/common/shared_imports.ts +++ b/x-pack/plugins/transform/common/shared_imports.ts @@ -8,7 +8,6 @@ export type { ChartData } from '@kbn/ml-plugin/common'; export { composeValidators, - isPopulatedObject, isRuntimeMappings, patternValidator, isRuntimeField, diff --git a/x-pack/plugins/transform/common/types/data_view.ts b/x-pack/plugins/transform/common/types/data_view.ts index 98787a1281dbb..b541254971c35 100644 --- a/x-pack/plugins/transform/common/types/data_view.ts +++ b/x-pack/plugins/transform/common/types/data_view.ts @@ -6,8 +6,7 @@ */ import type { DataView } from '@kbn/data-views-plugin/common'; - -import { isPopulatedObject } from '../shared_imports'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; // Custom minimal type guard for DataView to check against the attributes used in transforms code. export function isDataView(arg: any): arg is DataView { diff --git a/x-pack/plugins/transform/common/types/transform.ts b/x-pack/plugins/transform/common/types/transform.ts index a196111bf6678..f4f9437e05d13 100644 --- a/x-pack/plugins/transform/common/types/transform.ts +++ b/x-pack/plugins/transform/common/types/transform.ts @@ -6,8 +6,8 @@ */ import type { EuiComboBoxOptionOption } from '@elastic/eui/src/components/combo_box/types'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { LatestFunctionConfig, PutTransformsRequestSchema } from '../api_schemas/transforms'; -import { isPopulatedObject } from '../shared_imports'; import type { PivotGroupByDict } from './pivot_group_by'; import type { PivotAggDict } from './pivot_aggs'; import type { TransformHealthAlertRule } from './alerting'; diff --git a/x-pack/plugins/transform/common/types/transform_stats.ts b/x-pack/plugins/transform/common/types/transform_stats.ts index 00ffa40b84d3b..2f9319201fd6b 100644 --- a/x-pack/plugins/transform/common/types/transform_stats.ts +++ b/x-pack/plugins/transform/common/types/transform_stats.ts @@ -5,8 +5,9 @@ * 2.0. */ +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; + import { TransformState, TRANSFORM_STATE } from '../constants'; -import { isPopulatedObject } from '../shared_imports'; import { TransformId } from './transform'; export interface TransformStats { diff --git a/x-pack/plugins/transform/common/utils/errors.ts b/x-pack/plugins/transform/common/utils/errors.ts index 2aff8f332b130..c9d81b740f721 100644 --- a/x-pack/plugins/transform/common/utils/errors.ts +++ b/x-pack/plugins/transform/common/utils/errors.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { isPopulatedObject } from '../shared_imports'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; export interface ErrorResponse { body: { diff --git a/x-pack/plugins/transform/public/app/common/pivot_aggs.ts b/x-pack/plugins/transform/public/app/common/pivot_aggs.ts index 9045191a779cb..4b986659fe633 100644 --- a/x-pack/plugins/transform/public/app/common/pivot_aggs.ts +++ b/x-pack/plugins/transform/public/app/common/pivot_aggs.ts @@ -8,13 +8,13 @@ import { FC } from 'react'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/data-plugin/common'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { AggName } from '../../../common/types/aggregations'; import type { Dictionary } from '../../../common/types/common'; import type { EsFieldName } from '../../../common/types/fields'; import type { PivotAgg, PivotSupportedAggs } from '../../../common/types/pivot_aggs'; import { PIVOT_SUPPORTED_AGGS } from '../../../common/types/pivot_aggs'; -import { isPopulatedObject } from '../../../common/shared_imports'; import { getAggFormConfig } from '../sections/create_transform/components/step_define/common/get_agg_form_config'; import { PivotAggsConfigFilter } from '../sections/create_transform/components/step_define/common/filter_agg/types'; diff --git a/x-pack/plugins/transform/public/app/common/pivot_group_by.ts b/x-pack/plugins/transform/public/app/common/pivot_group_by.ts index dd9a63088e791..b0fa78e8a902f 100644 --- a/x-pack/plugins/transform/public/app/common/pivot_group_by.ts +++ b/x-pack/plugins/transform/public/app/common/pivot_group_by.ts @@ -6,11 +6,11 @@ */ import { KBN_FIELD_TYPES } from '@kbn/data-plugin/common'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { AggName } from '../../../common/types/aggregations'; import { Dictionary } from '../../../common/types/common'; import { EsFieldName } from '../../../common/types/fields'; import { GenericAgg } from '../../../common/types/pivot_group_by'; -import { isPopulatedObject } from '../../../common/shared_imports'; import { PivotAggsConfigWithUiSupport } from './pivot_aggs'; export enum PIVOT_SUPPORTED_GROUP_BY_AGGS { diff --git a/x-pack/plugins/transform/public/app/common/request.ts b/x-pack/plugins/transform/public/app/common/request.ts index 350f57a3bcf58..4700e42a3d946 100644 --- a/x-pack/plugins/transform/public/app/common/request.ts +++ b/x-pack/plugins/transform/public/app/common/request.ts @@ -9,6 +9,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { HttpFetchError } from '@kbn/core/public'; import type { DataView } from '@kbn/data-views-plugin/public'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { DEFAULT_CONTINUOUS_MODE_DELAY, @@ -23,7 +24,6 @@ import type { PutTransformsPivotRequestSchema, PutTransformsRequestSchema, } from '../../../common/api_schemas/transforms'; -import { isPopulatedObject } from '../../../common/shared_imports'; import { DateHistogramAgg, HistogramAgg, TermsAgg } from '../../../common/types/pivot_group_by'; import { isDataView } from '../../../common/types/data_view'; diff --git a/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts b/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts index 659d525643965..c3db9834a5b54 100644 --- a/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts +++ b/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts @@ -6,9 +6,9 @@ */ import { i18n } from '@kbn/i18n'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { Privileges } from '../../../../../common/types/privileges'; -import { isPopulatedObject } from '../../../../../common/shared_imports'; export interface Capabilities { canGetTransform: boolean; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx index 649683182dcab..59f80d743a9a3 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx @@ -30,6 +30,7 @@ import { DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/public'; import { DuplicateDataViewError } from '@kbn/data-plugin/public'; import type { RuntimeField } from '@kbn/data-views-plugin/common'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { PutTransformsResponseSchema } from '../../../../../../common/api_schemas/transforms'; import { isGetTransformsStatsResponseSchema, @@ -49,7 +50,6 @@ import { PutTransformsLatestRequestSchema, PutTransformsPivotRequestSchema, } from '../../../../../../common/api_schemas/transforms'; -import { isPopulatedObject } from '../../../../../../common/shared_imports'; import { isContinuousTransform, isLatestTransform } from '../../../../../../common/types/transform'; import { TransformAlertFlyout } from '../../../../../alerting/transform_alerting_flyout'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.tsx index 6f590a0e17892..55ecc18863aa5 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.tsx @@ -10,13 +10,13 @@ import { EuiFormRow, EuiIcon, EuiSelect, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import useUpdateEffect from 'react-use/lib/useUpdateEffect'; import { DataView } from '@kbn/data-views-plugin/public'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { CreateTransformWizardContext } from '../../../../wizard/wizard'; import { commonFilterAggs, filterAggsFieldSupport } from '../constants'; import { getFilterAggTypeConfig } from '../config'; import type { FilterAggType, PivotAggsConfigFilter } from '../types'; import type { RuntimeMappings } from '../../types'; import { getKibanaFieldTypeFromEsType } from '../../get_pivot_dropdown_options'; -import { isPopulatedObject } from '../../../../../../../../../common/shared_imports'; /** * Resolves supported filters for provided field. diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/top_metrics_agg/config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/top_metrics_agg/config.ts index 56d17e7973e16..49be7b299712b 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/top_metrics_agg/config.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/top_metrics_agg/config.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { isPivotAggsConfigWithUiSupport, isSpecialSortField, @@ -16,7 +17,6 @@ import { } from '../../../../../../common/pivot_aggs'; import { PivotAggsConfigTopMetrics } from './types'; import { TopMetricsAggForm } from './components/top_metrics_agg_form'; -import { isPopulatedObject } from '../../../../../../../../common/shared_imports'; /** * Gets initial basic configuration of the top_metrics aggregation. diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts index e97d47864313c..a8a9b5c1e35b0 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts @@ -6,6 +6,7 @@ */ import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { EsFieldName } from '../../../../../../../common/types/fields'; @@ -24,7 +25,7 @@ import { } from '../../../../../../../common/types/transform'; import { LatestFunctionConfig } from '../../../../../../../common/api_schemas/transforms'; -import { isPopulatedObject, RUNTIME_FIELD_TYPES } from '../../../../../../../common/shared_imports'; +import { RUNTIME_FIELD_TYPES } from '../../../../../../../common/shared_imports'; export interface ErrorMessage { query: string; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts index eb9adbb45b5b4..a2dc9148bf03f 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts @@ -11,8 +11,8 @@ import { merge } from 'lodash'; import { useReducer } from 'react'; import { i18n } from '@kbn/i18n'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; -import { isPopulatedObject } from '../../../../../../common/shared_imports'; import { PostTransformsUpdateRequestSchema } from '../../../../../../common/api_schemas/update_transforms'; import { DEFAULT_TRANSFORM_FREQUENCY, diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx index 84110e67d701e..6477e33a5c5a7 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx @@ -10,6 +10,7 @@ import React, { FC } from 'react'; import { EuiButtonEmpty, EuiTabbedContent } from '@elastic/eui'; import { Optional } from '@kbn/utility-types'; import { i18n } from '@kbn/i18n'; +import { stringHash } from '@kbn/ml-string-hash'; import moment from 'moment-timezone'; import { TransformListRow } from '../../../../common'; @@ -28,23 +29,6 @@ function getItemDescription(value: any) { return value.toString(); } -/** - * Creates a deterministic number based hash out of a string. - */ -export function stringHash(str: string): number { - let hash = 0; - let chr = 0; - if (str.length === 0) { - return hash; - } - for (let i = 0; i < str.length; i++) { - chr = str.charCodeAt(i); - hash = (hash << 5) - hash + chr; // eslint-disable-line no-bitwise - hash |= 0; // eslint-disable-line no-bitwise - } - return hash < 0 ? hash * -2 : hash; -} - type Item = SectionItem; interface Props { diff --git a/x-pack/plugins/transform/server/routes/api/transforms_nodes.ts b/x-pack/plugins/transform/server/routes/api/transforms_nodes.ts index 426dc3d4fa342..a5f8f014430c7 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms_nodes.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms_nodes.ts @@ -7,8 +7,9 @@ import Boom from '@hapi/boom'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; + import { NODES_INFO_PRIVILEGES } from '../../../common/constants'; -import { isPopulatedObject } from '../../../common/shared_imports'; import { RouteDependencies } from '../../types'; diff --git a/x-pack/test/api_integration/apis/ml/modules/get_module.ts b/x-pack/test/api_integration/apis/ml/modules/get_module.ts index 5810b4bf7e6c8..4ff71201d9ad9 100644 --- a/x-pack/test/api_integration/apis/ml/modules/get_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/get_module.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; -import { isPopulatedObject } from '@kbn/ml-plugin/common/util/object_utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { USER } from '../../../../functional/services/ml/security_common'; import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; diff --git a/yarn.lock b/yarn.lock index 2c9dcd462951b..5233110b1f99f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3275,6 +3275,18 @@ version "0.0.0" uid "" +"@kbn/ml-agg-utils@link:bazel-bin/x-pack/packages/ml/agg_utils": + version "0.0.0" + uid "" + +"@kbn/ml-is-populated-object@link:bazel-bin/x-pack/packages/ml/is_populated_object": + version "0.0.0" + uid "" + +"@kbn/ml-string-hash@link:bazel-bin/x-pack/packages/ml/string_hash": + version "0.0.0" + uid "" + "@kbn/monaco@link:bazel-bin/packages/kbn-monaco": version "0.0.0" uid "" @@ -6694,6 +6706,18 @@ version "0.0.0" uid "" +"@types/kbn__ml-agg-utils@link:bazel-bin/x-pack/packages/ml/agg_utils/npm_module_types": + version "0.0.0" + uid "" + +"@types/kbn__ml-is-populated-object@link:bazel-bin/x-pack/packages/ml/is_populated_object/npm_module_types": + version "0.0.0" + uid "" + +"@types/kbn__ml-string-hash@link:bazel-bin/x-pack/packages/ml/string_hash/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__monaco@link:bazel-bin/packages/kbn-monaco/npm_module_types": version "0.0.0" uid "" From ca532310f254e765c02fe85c9000eae4248158ff Mon Sep 17 00:00:00 2001 From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> Date: Thu, 23 Jun 2022 10:51:12 +0200 Subject: [PATCH 14/54] [Fleet] fix missing fleet server policy from enroll command (#134980) --- .../fleet/components/fleet_server_instructions/advanced_tab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/advanced_tab.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/advanced_tab.tsx index 8ecdffce3ed44..02cc458295677 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/advanced_tab.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/advanced_tab.tsx @@ -63,7 +63,7 @@ export const AdvancedTab: React.FunctionComponent = ({ selecte isFleetServerReady, serviceToken, fleetServerHost: fleetServerHostForm.fleetServerHost, - fleetServerPolicyId, + fleetServerPolicyId: fleetServerPolicyId || selectedPolicyId, deploymentMode, disabled: !Boolean(serviceToken), }), From 8133605b894804cd834aea6152980c6164b9768f Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Thu, 23 Jun 2022 12:46:07 +0300 Subject: [PATCH 15/54] [EBT] Enrich kibana loaded with timings (#134770) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add timings to kibana loaded event * jest * PR failures * code review * docs * add first_app_nav and first_app * tests * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * Update src/core/public/core_system.ts Co-authored-by: Alejandro Fernández Haro * Update src/core/public/core_system.ts Co-authored-by: Alejandro Fernández Haro * review @afjaro * typo * KBN_LOAD_MARKS Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Alejandro Fernández Haro --- src/core/public/core_system.test.ts | 27 ++++++- src/core/public/core_system.ts | 74 ++++++++++++++++++- src/core/public/kbn_bootstrap.test.ts | 1 + src/core/public/kbn_bootstrap.ts | 5 ++ src/core/public/public.api.md | 2 +- src/core/public/utils/consts.ts | 10 +++ src/core/public/utils/index.ts | 1 + .../render_template.test.ts.snap | 4 + .../rendering/bootstrap/render_template.ts | 4 + .../from_the_browser/loaded_kibana.ts | 15 ++++ 10 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 src/core/public/utils/consts.ts diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 2a57364c9f93f..76f972d174ce8 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -70,6 +70,19 @@ const defaultCoreSystemParams = { beforeEach(() => { jest.clearAllMocks(); MockPluginsService.getOpaqueIds.mockReturnValue(new Map()); + + window.performance.mark = jest.fn(); + window.performance.clearMarks = jest.fn(); + window.performance.getEntriesByName = jest.fn().mockReturnValue([ + { + detail: 'load_started', + startTime: 456, + }, + { + detail: 'bootstrap_started', + startTime: 123, + }, + ]); }); function createCoreSystem(params = {}) { @@ -221,7 +234,9 @@ describe('#start()', () => { }); await core.setup(); - await core.start(); + + const services = await core.start(); + await services?.application.navigateToApp('home'); } it('clears the children of the rootDomElement and appends container for rendering service with #kibana-body, notifications, overlays', async () => { @@ -233,16 +248,22 @@ describe('#start()', () => { ); }); - it('reports the event Loaded Kibana', async () => { + it('reports the event Loaded Kibana and clears marks', async () => { await startCore(); expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledTimes(1); expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledWith('Loaded Kibana', { kibana_version: '1.2.3', + load_started: 456, + bootstrap_started: 123, }); + + expect(window.performance.clearMarks).toHaveBeenCalledTimes(1); }); it('reports the event Loaded Kibana (with memory)', async () => { fetchOptionalMemoryInfoMock.mockReturnValue({ + load_started: 456, + bootstrap_started: 123, memory_js_heap_size_limit: 3, memory_js_heap_size_total: 2, memory_js_heap_size_used: 1, @@ -251,6 +272,8 @@ describe('#start()', () => { await startCore(); expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledTimes(1); expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledWith('Loaded Kibana', { + load_started: 456, + bootstrap_started: 123, kibana_version: '1.2.3', memory_js_heap_size_limit: 3, memory_js_heap_size_total: 2, diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 5402e1cd58504..bdf94d953b659 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -15,7 +15,7 @@ import { } from '@kbn/core-injected-metadata-browser-internal'; import { DocLinksService } from '@kbn/core-doc-links-browser-internal'; import { ThemeService } from '@kbn/core-theme-browser-internal'; -import type { AnalyticsServiceSetup } from '@kbn/core-analytics-browser'; +import type { AnalyticsServiceSetup, AnalyticsServiceStart } from '@kbn/core-analytics-browser'; import { AnalyticsService } from '@kbn/core-analytics-browser-internal'; import { I18nService } from '@kbn/core-i18n-browser-internal'; import { CoreSetup, CoreStart } from '.'; @@ -35,6 +35,7 @@ import { CoreApp } from './core_app'; import type { InternalApplicationSetup, InternalApplicationStart } from './application/types'; import { ExecutionContextService } from './execution_context'; import { fetchOptionalMemoryInfo } from './fetch_optional_memory_info'; +import { KBN_LOAD_MARKS } from './utils'; interface Params { rootDomElement: HTMLElement; @@ -124,6 +125,30 @@ export class CoreSystem { this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins); this.coreApp = new CoreApp(this.coreContext); + + performance.mark(KBN_LOAD_MARKS, { + detail: 'core_created', + }); + } + + private getLoadMarksInfo() { + if (!performance) return []; + const reportData: Record = {}; + const marks = performance.getEntriesByName(KBN_LOAD_MARKS); + for (const mark of marks) { + reportData[(mark as PerformanceMark).detail] = mark.startTime; + } + + return reportData; + } + + private reportKibanaLoadedEvent(analytics: AnalyticsServiceStart) { + analytics.reportEvent('Loaded Kibana', { + kibana_version: this.coreContext.env.packageInfo.version, + ...fetchOptionalMemoryInfo(), + ...this.getLoadMarksInfo(), + }); + performance.clearMarks(KBN_LOAD_MARKS); } public async setup() { @@ -171,6 +196,10 @@ export class CoreSystem { // Services that do not expose contracts at setup await this.plugins.setup(core); + performance.mark(KBN_LOAD_MARKS, { + detail: 'setup_done', + }); + return { fatalErrors: this.fatalErrorsSetup }; } catch (error) { if (this.fatalErrorsSetup) { @@ -267,9 +296,19 @@ export class CoreSystem { targetDomElement: coreUiTargetDomElement, }); - analytics.reportEvent('Loaded Kibana', { - kibana_version: this.coreContext.env.packageInfo.version, - ...fetchOptionalMemoryInfo(), + performance.mark(KBN_LOAD_MARKS, { + detail: 'start_done', + }); + + // Wait for the first app navigation to report Kibana Loaded + const appSub = application.currentAppId$.subscribe((appId) => { + if (appId === undefined) return; + + performance.mark(KBN_LOAD_MARKS, { + detail: 'first_app_nav', + }); + this.reportKibanaLoadedEvent(analytics); + appSub.unsubscribe(); }); return { @@ -323,6 +362,33 @@ export class CoreSystem { type: 'long', _meta: { description: 'The used size of the heap', optional: true }, }, + load_started: { + type: 'long', + _meta: { description: 'When the render template starts loading assets', optional: true }, + }, + bootstrap_started: { + type: 'long', + _meta: { description: 'When kbnBootstrap callback is called', optional: true }, + }, + core_created: { + type: 'long', + _meta: { description: 'When core system is created', optional: true }, + }, + setup_done: { + type: 'long', + _meta: { description: 'When core system setup is complete', optional: true }, + }, + start_done: { + type: 'long', + _meta: { description: 'When core system start is complete', optional: true }, + }, + first_app_nav: { + type: 'long', + _meta: { + description: 'When the application emits the first app navigation', + optional: true, + }, + }, }, }); } diff --git a/src/core/public/kbn_bootstrap.test.ts b/src/core/public/kbn_bootstrap.test.ts index 03096daf09c39..f32bb21cd041a 100644 --- a/src/core/public/kbn_bootstrap.test.ts +++ b/src/core/public/kbn_bootstrap.test.ts @@ -22,6 +22,7 @@ describe('kbn_bootstrap', () => { beforeEach(() => { jest.clearAllMocks(); + window.performance.mark = jest.fn(); }); it('does not report a fatal error if apm load fails', async () => { diff --git a/src/core/public/kbn_bootstrap.ts b/src/core/public/kbn_bootstrap.ts index 38e95188f4c06..79283daaf9a3a 100644 --- a/src/core/public/kbn_bootstrap.ts +++ b/src/core/public/kbn_bootstrap.ts @@ -9,9 +9,14 @@ import { i18n } from '@kbn/i18n'; import { CoreSystem } from './core_system'; import { ApmSystem } from './apm_system'; +import { KBN_LOAD_MARKS } from './utils'; /** @internal */ export async function __kbnBootstrap__() { + performance.mark(KBN_LOAD_MARKS, { + detail: 'bootstrap_started', + }); + const injectedMetadata = JSON.parse( document.querySelector('kbn-injected-metadata')!.getAttribute('data')! ); diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 279d97d571262..d0a3bc2ad19cf 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1554,6 +1554,6 @@ export interface UserProvidedValues { // Warnings were encountered during analysis: // -// src/core/public/core_system.ts:186:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts +// src/core/public/core_system.ts:202:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/public/utils/consts.ts b/src/core/public/utils/consts.ts new file mode 100644 index 0000000000000..8372eafec8147 --- /dev/null +++ b/src/core/public/utils/consts.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** @internal */ +export const KBN_LOAD_MARKS = 'kbnLoad'; diff --git a/src/core/public/utils/index.ts b/src/core/public/utils/index.ts index d28a8dcc37501..4fb9c50f715c6 100644 --- a/src/core/public/utils/index.ts +++ b/src/core/public/utils/index.ts @@ -9,3 +9,4 @@ export { Sha256 } from './crypto'; export { MountWrapper, mountReactNode } from './mount'; export { CoreContextProvider } from './core_context_provider'; +export { KBN_LOAD_MARKS } from './consts'; diff --git a/src/core/server/rendering/bootstrap/__snapshots__/render_template.test.ts.snap b/src/core/server/rendering/bootstrap/__snapshots__/render_template.test.ts.snap index 83aacda2b599a..f7e28eebd1a61 100644 --- a/src/core/server/rendering/bootstrap/__snapshots__/render_template.test.ts.snap +++ b/src/core/server/rendering/bootstrap/__snapshots__/render_template.test.ts.snap @@ -104,6 +104,10 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { }); } + performance.mark('kbnLoad', { + detail: 'load_started', + }) + load([ '/js-1','/js-2' ], function () { diff --git a/src/core/server/rendering/bootstrap/render_template.ts b/src/core/server/rendering/bootstrap/render_template.ts index 14127017b1c0f..996aacd5e3ede 100644 --- a/src/core/server/rendering/bootstrap/render_template.ts +++ b/src/core/server/rendering/bootstrap/render_template.ts @@ -120,6 +120,10 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { }); } + performance.mark('kbnLoad', { + detail: 'load_started', + }) + load([ ${jsDependencyPaths.map((path) => `'${path}'`).join(',')} ], function () { diff --git a/test/analytics/tests/instrumented_events/from_the_browser/loaded_kibana.ts b/test/analytics/tests/instrumented_events/from_the_browser/loaded_kibana.ts index 53493b99ad1ad..9b7310529eed3 100644 --- a/test/analytics/tests/instrumented_events/from_the_browser/loaded_kibana.ts +++ b/test/analytics/tests/instrumented_events/from_the_browser/loaded_kibana.ts @@ -25,7 +25,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(event.properties).to.have.property('kibana_version'); expect(event.properties.kibana_version).to.be.a('string'); + // Kibana Loaded timings + expect(event.properties).to.have.property('load_started'); + expect(event.properties.load_started).to.be.a('number'); + expect(event.properties).to.have.property('bootstrap_started'); + expect(event.properties.bootstrap_started).to.be.a('number'); + expect(event.properties).to.have.property('core_created'); + expect(event.properties.core_created).to.be.a('number'); + expect(event.properties).to.have.property('setup_done'); + expect(event.properties.setup_done).to.be.a('number'); + expect(event.properties).to.have.property('start_done'); + expect(event.properties.start_done).to.be.a('number'); + expect(event.properties).to.have.property('first_app_nav'); + expect(event.properties.start_done).to.be.a('number'); + if (browser.isChromium) { + // Kibana Loaded memory expect(event.properties).to.have.property('memory_js_heap_size_limit'); expect(event.properties.memory_js_heap_size_limit).to.be.a('number'); expect(event.properties).to.have.property('memory_js_heap_size_total'); From db728b12a3ae8ea7e2e9a2d12dedbddf63e87e67 Mon Sep 17 00:00:00 2001 From: Mat Schaffer Date: Thu, 23 Jun 2022 18:47:25 +0900 Subject: [PATCH 16/54] Ensure monitoring indices get cleaned during tests (#134978) --- .../monitoring/elasticsearch_settings/set_collection_enabled.js | 2 +- .../test/functional/apps/monitoring/enable_monitoring/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/api_integration/apis/monitoring/elasticsearch_settings/set_collection_enabled.js b/x-pack/test/api_integration/apis/monitoring/elasticsearch_settings/set_collection_enabled.js index ea94031c782c3..54c3eaa7f9049 100644 --- a/x-pack/test/api_integration/apis/monitoring/elasticsearch_settings/set_collection_enabled.js +++ b/x-pack/test/api_integration/apis/monitoring/elasticsearch_settings/set_collection_enabled.js @@ -28,7 +28,7 @@ export default function ({ getService }) { }; await esSupertest.put('/_cluster/settings').send(disableCollection).expect(200); - await esDeleteAllIndices('/.monitoring-*'); + await esDeleteAllIndices('.monitoring-*'); }); it('should set collection.enabled to true', async () => { diff --git a/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js b/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js index cce6401453d21..c60ef8617f16d 100644 --- a/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js @@ -40,7 +40,7 @@ export default function ({ getService, getPageObjects }) { }; await esSupertest.put('/_cluster/settings').send(disableCollection).expect(200); - await esDeleteAllIndices('/.monitoring-*'); + await esDeleteAllIndices('.monitoring-*'); }); it('Monitoring enabled', async function () { From 88c25a9373a6c853eb0e91ef3163ef9304526d9f Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+dimaanj@users.noreply.github.com> Date: Thu, 23 Jun 2022 12:54:28 +0300 Subject: [PATCH 17/54] [Discover] Cleanup uses of legacy table in functional tests (#134638) * [Discover] extract doc table tests into a new folder * [Discover] switch to data grid * [Discover] apply suggestions * [Discover] adapt scripted fields tests for data grid * [Discover] apply suggestions * [Discover] apply for another part --- .../apps/context/_context_navigation.ts | 1 - test/functional/apps/context/_date_nanos.ts | 1 - .../context/_date_nanos_custom_timestamp.ts | 1 - .../apps/context/_discover_navigation.ts | 1 - test/functional/apps/context/_filters.ts | 9 - test/functional/apps/context/_size.ts | 1 - .../discover/_context_encoded_url_params.ts | 2 +- test/functional/apps/discover/_data_grid.ts | 6 +- .../apps/discover/_data_grid_context.ts | 2 +- .../discover/_data_grid_copy_to_clipboard.ts | 1 - .../discover/_data_grid_doc_navigation.ts | 2 +- .../apps/discover/_data_grid_doc_table.ts | 1 - .../apps/discover/_data_grid_field_data.ts | 2 +- .../apps/discover/_data_grid_pagination.ts | 2 +- .../apps/discover/_discover_fields_api.ts | 6 +- test/functional/apps/discover/_field_data.ts | 34 -- .../discover/_field_data_with_fields_api.ts | 29 -- .../_classic_table_doc_navigation.ts | 2 +- .../discover/classic/_discover_fields_api.ts | 91 ++++ .../apps/discover/{ => classic}/_doc_table.ts | 6 +- .../{ => classic}/_doc_table_newline.ts | 7 +- .../apps/discover/classic/_field_data.ts | 62 +++ .../classic/_field_data_with_fields_api.ts | 46 ++ test/functional/apps/discover/index.ts | 9 +- .../apps/management/_scripted_fields.ts | 77 ++- .../_scripted_fields_classic_table.ts | 459 ++++++++++++++++++ test/functional/apps/management/index.ts | 1 + .../apps/discover/value_suggestions.ts | 10 +- .../value_suggestions_non_timebased.ts | 2 +- 29 files changed, 742 insertions(+), 131 deletions(-) rename test/functional/apps/discover/{ => classic}/_classic_table_doc_navigation.ts (97%) create mode 100644 test/functional/apps/discover/classic/_discover_fields_api.ts rename test/functional/apps/discover/{ => classic}/_doc_table.ts (98%) rename test/functional/apps/discover/{ => classic}/_doc_table_newline.ts (92%) create mode 100644 test/functional/apps/discover/classic/_field_data.ts create mode 100644 test/functional/apps/discover/classic/_field_data_with_fields_api.ts create mode 100644 test/functional/apps/management/_scripted_fields_classic_table.ts diff --git a/test/functional/apps/context/_context_navigation.ts b/test/functional/apps/context/_context_navigation.ts index 15de70882086e..1bda70cc558ee 100644 --- a/test/functional/apps/context/_context_navigation.ts +++ b/test/functional/apps/context/_context_navigation.ts @@ -30,7 +30,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async () => { await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await kibanaServer.uiSettings.update({ - 'doc_table:legacy': false, defaultIndex: 'logstash-*', }); await PageObjects.common.navigateToApp('discover'); diff --git a/test/functional/apps/context/_date_nanos.ts b/test/functional/apps/context/_date_nanos.ts index 969a7dcecd5c3..b486dd77ecef8 100644 --- a/test/functional/apps/context/_date_nanos.ts +++ b/test/functional/apps/context/_date_nanos.ts @@ -30,7 +30,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.update({ 'context:defaultSize': `${TEST_DEFAULT_CONTEXT_SIZE}`, 'context:step': `${TEST_STEP_SIZE}`, - 'doc_table:legacy': false, }); }); diff --git a/test/functional/apps/context/_date_nanos_custom_timestamp.ts b/test/functional/apps/context/_date_nanos_custom_timestamp.ts index b8af57e40a3e3..b91aafa89dabf 100644 --- a/test/functional/apps/context/_date_nanos_custom_timestamp.ts +++ b/test/functional/apps/context/_date_nanos_custom_timestamp.ts @@ -32,7 +32,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.update({ 'context:defaultSize': `${TEST_DEFAULT_CONTEXT_SIZE}`, 'context:step': `${TEST_STEP_SIZE}`, - 'doc_table:legacy': false, }); }); diff --git a/test/functional/apps/context/_discover_navigation.ts b/test/functional/apps/context/_discover_navigation.ts index 5add260d8f4d0..46f03b512bff3 100644 --- a/test/functional/apps/context/_discover_navigation.ts +++ b/test/functional/apps/context/_discover_navigation.ts @@ -37,7 +37,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async () => { await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await kibanaServer.uiSettings.update({ - 'doc_table:legacy': false, defaultIndex: 'logstash-*', }); await PageObjects.common.navigateToApp('discover'); diff --git a/test/functional/apps/context/_filters.ts b/test/functional/apps/context/_filters.ts index fe5b27f2f8055..e8a8675e85f82 100644 --- a/test/functional/apps/context/_filters.ts +++ b/test/functional/apps/context/_filters.ts @@ -19,19 +19,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const filterBar = getService('filterBar'); const testSubjects = getService('testSubjects'); const retry = getService('retry'); - const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'context']); describe('context filters', function contextSize() { - before(async function () { - await kibanaServer.uiSettings.update({ 'doc_table:legacy': false }); - }); - - after(async function () { - await kibanaServer.uiSettings.replace({}); - }); - beforeEach(async function () { await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_ID, { columns: TEST_COLUMN_NAMES, diff --git a/test/functional/apps/context/_size.ts b/test/functional/apps/context/_size.ts index 563af7e07857e..853d4ebde79de 100644 --- a/test/functional/apps/context/_size.ts +++ b/test/functional/apps/context/_size.ts @@ -29,7 +29,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.update({ 'context:defaultSize': `${TEST_DEFAULT_CONTEXT_SIZE}`, 'context:step': `${TEST_STEP_SIZE}`, - 'doc_table:legacy': false, }); await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_ID); }); diff --git a/test/functional/apps/discover/_context_encoded_url_params.ts b/test/functional/apps/discover/_context_encoded_url_params.ts index 293a1ee0b5e28..cff83b2d6f645 100644 --- a/test/functional/apps/discover/_context_encoded_url_params.ts +++ b/test/functional/apps/discover/_context_encoded_url_params.ts @@ -41,7 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('discover'); }); - it('should navigate correctly when ', async () => { + it('should navigate correctly', async () => { await PageObjects.discover.selectIndexPattern('context-encoded-param'); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.discover.waitForDocTableLoadingComplete(); diff --git a/test/functional/apps/discover/_data_grid.ts b/test/functional/apps/discover/_data_grid.ts index 198691f3b8477..96085f09186f6 100644 --- a/test/functional/apps/discover/_data_grid.ts +++ b/test/functional/apps/discover/_data_grid.ts @@ -19,7 +19,7 @@ export default function ({ const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); const kibanaServer = getService('kibanaServer'); - const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false }; + const defaultSettings = { defaultIndex: 'logstash-*' }; const testSubjects = getService('testSubjects'); before(async function () { @@ -31,10 +31,6 @@ export default function ({ await PageObjects.timePicker.setDefaultAbsoluteRange(); }); - after(async function () { - await kibanaServer.uiSettings.replace({ 'doc_table:legacy': true }); - }); - it('can add fields to the table', async function () { const getTitles = async () => (await testSubjects.getVisibleText('dataGridHeader')).replace(/\s|\r?\n|\r/g, ' '); diff --git a/test/functional/apps/discover/_data_grid_context.ts b/test/functional/apps/discover/_data_grid_context.ts index d12ada2070cff..c2628026dfdda 100644 --- a/test/functional/apps/discover/_data_grid_context.ts +++ b/test/functional/apps/discover/_data_grid_context.ts @@ -27,7 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'dashboard', 'header', ]); - const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false }; + const defaultSettings = { defaultIndex: 'logstash-*' }; const kibanaServer = getService('kibanaServer'); const esArchiver = getService('esArchiver'); const dashboardAddPanel = getService('dashboardAddPanel'); diff --git a/test/functional/apps/discover/_data_grid_copy_to_clipboard.ts b/test/functional/apps/discover/_data_grid_copy_to_clipboard.ts index f202595729cfb..ec359e3c569db 100644 --- a/test/functional/apps/discover/_data_grid_copy_to_clipboard.ts +++ b/test/functional/apps/discover/_data_grid_copy_to_clipboard.ts @@ -19,7 +19,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker', 'dashboard']); const defaultSettings = { defaultIndex: 'logstash-*', - 'doc_table:legacy': false, }; describe('discover data grid supports copy to clipboard', function describeIndexTests() { diff --git a/test/functional/apps/discover/_data_grid_doc_navigation.ts b/test/functional/apps/discover/_data_grid_doc_navigation.ts index 2da6db97aa13f..3c5c8b3967cb0 100644 --- a/test/functional/apps/discover/_data_grid_doc_navigation.ts +++ b/test/functional/apps/discover/_data_grid_doc_navigation.ts @@ -17,7 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const retry = getService('retry'); const kibanaServer = getService('kibanaServer'); - const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false }; + const defaultSettings = { defaultIndex: 'logstash-*' }; describe('discover data grid doc link', function () { before(async () => { diff --git a/test/functional/apps/discover/_data_grid_doc_table.ts b/test/functional/apps/discover/_data_grid_doc_table.ts index bb0cd56298c20..6918edc8285d8 100644 --- a/test/functional/apps/discover/_data_grid_doc_table.ts +++ b/test/functional/apps/discover/_data_grid_doc_table.ts @@ -21,7 +21,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker', 'dashboard']); const defaultSettings = { defaultIndex: 'logstash-*', - 'doc_table:legacy': false, }; const testSubjects = getService('testSubjects'); diff --git a/test/functional/apps/discover/_data_grid_field_data.ts b/test/functional/apps/discover/_data_grid_field_data.ts index 4a4e06e28c321..84d1c81f8ee68 100644 --- a/test/functional/apps/discover/_data_grid_field_data.ts +++ b/test/functional/apps/discover/_data_grid_field_data.ts @@ -16,7 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const toasts = getService('toasts'); const queryBar = getService('queryBar'); const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); - const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false }; + const defaultSettings = { defaultIndex: 'logstash-*' }; const dataGrid = getService('dataGrid'); describe('discover data grid field data tests', function describeIndexTests() { diff --git a/test/functional/apps/discover/_data_grid_pagination.ts b/test/functional/apps/discover/_data_grid_pagination.ts index da9faa24bb151..7b0fc40e94ab8 100644 --- a/test/functional/apps/discover/_data_grid_pagination.ts +++ b/test/functional/apps/discover/_data_grid_pagination.ts @@ -14,7 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const dataGrid = getService('dataGrid'); const PageObjects = getPageObjects(['settings', 'common', 'discover', 'header', 'timePicker']); - const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false }; + const defaultSettings = { defaultIndex: 'logstash-*' }; const testSubjects = getService('testSubjects'); const retry = getService('retry'); diff --git a/test/functional/apps/discover/_discover_fields_api.ts b/test/functional/apps/discover/_discover_fields_api.ts index fb3ee3b9858d3..e64280c1977b5 100644 --- a/test/functional/apps/discover/_discover_fields_api.ts +++ b/test/functional/apps/discover/_discover_fields_api.ts @@ -14,11 +14,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); + const dataGrid = getService('dataGrid'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker', 'settings']); const defaultSettings = { defaultIndex: 'logstash-*', 'discover:searchFieldsFromSource': false, - 'doc_table:legacy': true, }; describe('discover uses fields API test', function describeIndexTests() { before(async function () { @@ -60,7 +60,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('displays _source viewer in doc viewer', async function () { - await PageObjects.discover.clickDocTableRowToggle(0); + await dataGrid.clickRowToggle(); await PageObjects.discover.isShowingDocViewer(); await PageObjects.discover.clickDocViewerTab(1); await PageObjects.discover.expectSourceViewerToExist(); @@ -74,7 +74,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setDefaultAbsoluteRange(); - expect(await PageObjects.discover.getDocHeader()).to.have.string('_source'); + expect(await PageObjects.discover.getDocHeader()).to.have.string('Document'); }); it('switches to Document column when fields API is used', async function () { diff --git a/test/functional/apps/discover/_field_data.ts b/test/functional/apps/discover/_field_data.ts index d13baf9948171..27b786a2abfc1 100644 --- a/test/functional/apps/discover/_field_data.ts +++ b/test/functional/apps/discover/_field_data.ts @@ -18,8 +18,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const queryBar = getService('queryBar'); const browser = getService('browser'); const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); - const find = getService('find'); - const testSubjects = getService('testSubjects'); describe('discover tab', function describeIndexTests() { this.tags('includeFirefox'); @@ -97,38 +95,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const marks = await PageObjects.discover.getMarks(); expect(marks.length).to.be(0); }); - - describe('legacy table tests', async function () { - before(async function () { - await kibanaServer.uiSettings.update({ 'doc_table:legacy': true }); - await PageObjects.common.navigateToApp('discover'); - }); - - after(async function () { - await kibanaServer.uiSettings.replace({}); - }); - it('doc view should show @timestamp and _source columns', async function () { - const expectedHeader = '@timestamp\n_source'; - const docHeader = await find.byCssSelector('thead > tr:nth-child(1)'); - const docHeaderText = await docHeader.getVisibleText(); - expect(docHeaderText).to.be(expectedHeader); - }); - - it('doc view should sort ascending', async function () { - const expectedTimeStamp = 'Sep 20, 2015 @ 00:00:00.000'; - await testSubjects.click('docTableHeaderFieldSort_@timestamp'); - - // we don't technically need this sleep here because the tryForTime will retry and the - // results will match on the 2nd or 3rd attempt, but that debug output is huge in this - // case and it can be avoided with just a few seconds sleep. - await PageObjects.common.sleep(2000); - await retry.try(async function tryingForTime() { - const row = await find.byCssSelector(`tr.kbnDocTable__row:nth-child(1)`); - const rowData = await row.getVisibleText(); - expect(rowData.startsWith(expectedTimeStamp)).to.be.ok(); - }); - }); - }); }); }); } diff --git a/test/functional/apps/discover/_field_data_with_fields_api.ts b/test/functional/apps/discover/_field_data_with_fields_api.ts index d7912d6d0959f..85bb26df24129 100644 --- a/test/functional/apps/discover/_field_data_with_fields_api.ts +++ b/test/functional/apps/discover/_field_data_with_fields_api.ts @@ -18,8 +18,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const queryBar = getService('queryBar'); const browser = getService('browser'); const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); - const find = getService('find'); - const testSubjects = getService('testSubjects'); describe('discover tab with new fields API', function describeIndexTests() { this.tags('includeFirefox'); @@ -104,33 +102,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(marks.length).to.be.above(0); expect(marks).to.contain('election'); }); - - describe('legacy table tests', async function () { - before(async function () { - await kibanaServer.uiSettings.update({ 'doc_table:legacy': true }); - await PageObjects.common.navigateToApp('discover'); - }); - - after(async function () { - await kibanaServer.uiSettings.replace({}); - }); - - it('doc view should sort ascending', async function () { - const expectedTimeStamp = 'Sep 20, 2015 @ 00:00:00.000'; - await testSubjects.click('docTableHeaderFieldSort_@timestamp'); - - // we don't technically need this sleep here because the tryForTime will retry and the - // results will match on the 2nd or 3rd attempt, but that debug output is huge in this - // case and it can be avoided with just a few seconds sleep. - await PageObjects.common.sleep(2000); - await retry.try(async function tryingForTime() { - const row = await find.byCssSelector(`tr.kbnDocTable__row:nth-child(1)`); - const rowData = await row.getVisibleText(); - - expect(rowData.startsWith(expectedTimeStamp)).to.be.ok(); - }); - }); - }); }); }); } diff --git a/test/functional/apps/discover/_classic_table_doc_navigation.ts b/test/functional/apps/discover/classic/_classic_table_doc_navigation.ts similarity index 97% rename from test/functional/apps/discover/_classic_table_doc_navigation.ts rename to test/functional/apps/discover/classic/_classic_table_doc_navigation.ts index c768d9600c189..a27d3df81d32f 100644 --- a/test/functional/apps/discover/_classic_table_doc_navigation.ts +++ b/test/functional/apps/discover/classic/_classic_table_doc_navigation.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const docTable = getService('docTable'); diff --git a/test/functional/apps/discover/classic/_discover_fields_api.ts b/test/functional/apps/discover/classic/_discover_fields_api.ts new file mode 100644 index 0000000000000..7108cc2911be8 --- /dev/null +++ b/test/functional/apps/discover/classic/_discover_fields_api.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const log = getService('log'); + const retry = getService('retry'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker', 'settings']); + const defaultSettings = { + defaultIndex: 'logstash-*', + 'discover:searchFieldsFromSource': false, + 'doc_table:legacy': true, + }; + describe('discover uses fields API test', function describeIndexTests() { + before(async function () { + log.debug('load kibana index with default index pattern'); + await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.uiSettings.replace(defaultSettings); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + }); + + after(async () => { + await kibanaServer.uiSettings.replace({}); + }); + + it('should correctly display documents', async function () { + log.debug('check if Document title exists in the grid'); + expect(await PageObjects.discover.getDocHeader()).to.have.string('Document'); + const rowData = await PageObjects.discover.getDocTableIndex(1); + log.debug('check the newest doc timestamp in UTC (check diff timezone in last test)'); + expect(rowData.startsWith('Sep 22, 2015 @ 23:50:13.253')).to.be.ok(); + const expectedHitCount = '14,004'; + await retry.try(async function () { + expect(await PageObjects.discover.getHitCount()).to.be(expectedHitCount); + }); + }); + + it('adding a column removes a default column', async function () { + await PageObjects.discover.clickFieldListItemAdd('_score'); + expect(await PageObjects.discover.getDocHeader()).to.have.string('_score'); + expect(await PageObjects.discover.getDocHeader()).not.to.have.string('Document'); + }); + + it('removing a column adds a default column', async function () { + await PageObjects.discover.clickFieldListItemRemove('_score'); + expect(await PageObjects.discover.getDocHeader()).not.to.have.string('_score'); + expect(await PageObjects.discover.getDocHeader()).to.have.string('Document'); + }); + + it('displays _source viewer in doc viewer', async function () { + await PageObjects.discover.clickDocTableRowToggle(0); + await PageObjects.discover.isShowingDocViewer(); + await PageObjects.discover.clickDocViewerTab(1); + await PageObjects.discover.expectSourceViewerToExist(); + }); + + it('switches to _source column when fields API is no longer used', async function () { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSettings(); + await PageObjects.settings.toggleAdvancedSettingCheckbox('discover:searchFieldsFromSource'); + + await PageObjects.common.navigateToApp('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + + expect(await PageObjects.discover.getDocHeader()).to.have.string('_source'); + }); + + it('switches to Document column when fields API is used', async function () { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSettings(); + await PageObjects.settings.toggleAdvancedSettingCheckbox('discover:searchFieldsFromSource'); + + await PageObjects.common.navigateToApp('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + + expect(await PageObjects.discover.getDocHeader()).to.have.string('Document'); + }); + }); +} diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/classic/_doc_table.ts similarity index 98% rename from test/functional/apps/discover/_doc_table.ts rename to test/functional/apps/discover/classic/_doc_table.ts index 321c41b92e9be..bfc813f2d822e 100644 --- a/test/functional/apps/discover/_doc_table.ts +++ b/test/functional/apps/discover/classic/_doc_table.ts @@ -7,7 +7,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); @@ -40,6 +40,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('discover'); }); + after(async function () { + await kibanaServer.uiSettings.replace({}); + }); + it('should show records by default', async function () { // with the default range the number of hits is ~14000 const rows = await PageObjects.discover.getDocTableRows(); diff --git a/test/functional/apps/discover/_doc_table_newline.ts b/test/functional/apps/discover/classic/_doc_table_newline.ts similarity index 92% rename from test/functional/apps/discover/_doc_table_newline.ts rename to test/functional/apps/discover/classic/_doc_table_newline.ts index a02d31d5c76f4..112fff800cd41 100644 --- a/test/functional/apps/discover/_doc_table_newline.ts +++ b/test/functional/apps/discover/classic/_doc_table_newline.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -35,8 +35,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await security.testUser.restoreDefaults(); await esArchiver.unload('test/functional/fixtures/es_archiver/message_with_newline'); await kibanaServer.savedObjects.cleanStandardList(); - await kibanaServer.uiSettings.unset('defaultIndex'); - await kibanaServer.uiSettings.unset('doc_table:legacy'); + await kibanaServer.uiSettings.replace({}); }); it('should break text on newlines', async function () { @@ -47,6 +46,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const heightWithoutNewline = await dscTableRows[0].getAttribute('clientHeight'); const heightWithNewline = await dscTableRows[1].getAttribute('clientHeight'); log.debug(`Without newlines: ${heightWithoutNewline}, With newlines: ${heightWithNewline}`); + + await PageObjects.common.sleep(10000); return Number(heightWithNewline) > Number(heightWithoutNewline); }); }); diff --git a/test/functional/apps/discover/classic/_field_data.ts b/test/functional/apps/discover/classic/_field_data.ts new file mode 100644 index 0000000000000..f574a36dea028 --- /dev/null +++ b/test/functional/apps/discover/classic/_field_data.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); + const find = getService('find'); + const testSubjects = getService('testSubjects'); + + describe('discover tab', function describeIndexTests() { + this.tags('includeFirefox'); + before(async function () { + await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.uiSettings.replace({ + defaultIndex: 'logstash-*', + 'discover:searchFieldsFromSource': true, + 'doc_table:legacy': true, + }); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await PageObjects.common.navigateToApp('discover'); + }); + + after(async function () { + await kibanaServer.uiSettings.replace({}); + }); + + it('doc view should show @timestamp and _source columns', async function () { + const expectedHeader = '@timestamp\n_source'; + const docHeader = await find.byCssSelector('thead > tr:nth-child(1)'); + const docHeaderText = await docHeader.getVisibleText(); + expect(docHeaderText).to.be(expectedHeader); + }); + + it('doc view should sort ascending', async function () { + const expectedTimeStamp = 'Sep 20, 2015 @ 00:00:00.000'; + await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + + // we don't technically need this sleep here because the tryForTime will retry and the + // results will match on the 2nd or 3rd attempt, but that debug output is huge in this + // case and it can be avoided with just a few seconds sleep. + await PageObjects.common.sleep(2000); + await retry.try(async function tryingForTime() { + const row = await find.byCssSelector(`tr.kbnDocTable__row:nth-child(1)`); + const rowData = await row.getVisibleText(); + expect(rowData.startsWith(expectedTimeStamp)).to.be.ok(); + }); + }); + }); +} diff --git a/test/functional/apps/discover/classic/_field_data_with_fields_api.ts b/test/functional/apps/discover/classic/_field_data_with_fields_api.ts new file mode 100644 index 0000000000000..efa680bfbfa3a --- /dev/null +++ b/test/functional/apps/discover/classic/_field_data_with_fields_api.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); + const find = getService('find'); + const testSubjects = getService('testSubjects'); + + describe('discover tab with new fields API', function describeIndexTests() { + before(async function () { + await kibanaServer.uiSettings.update({ 'doc_table:legacy': true }); + await PageObjects.common.navigateToApp('discover'); + }); + + after(async function () { + await kibanaServer.uiSettings.replace({}); + }); + + it('doc view should sort ascending', async function () { + const expectedTimeStamp = 'Sep 20, 2015 @ 00:00:00.000'; + await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + + // we don't technically need this sleep here because the tryForTime will retry and the + // results will match on the 2nd or 3rd attempt, but that debug output is huge in this + // case and it can be avoided with just a few seconds sleep. + await PageObjects.common.sleep(2000); + await retry.try(async function tryingForTime() { + const row = await find.byCssSelector(`tr.kbnDocTable__row:nth-child(1)`); + const rowData = await row.getVisibleText(); + + expect(rowData.startsWith(expectedTimeStamp)).to.be.ok(); + }); + }); + }); +} diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts index db72e270fd337..cd2742abe8e13 100644 --- a/test/functional/apps/discover/index.ts +++ b/test/functional/apps/discover/index.ts @@ -30,21 +30,24 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_discover')); loadTestFile(require.resolve('./_discover_accessibility')); loadTestFile(require.resolve('./_discover_histogram')); - loadTestFile(require.resolve('./_doc_table')); - loadTestFile(require.resolve('./_doc_table_newline')); + loadTestFile(require.resolve('./classic/_doc_table')); + loadTestFile(require.resolve('./classic/_doc_table_newline')); loadTestFile(require.resolve('./_filter_editor')); loadTestFile(require.resolve('./_errors')); loadTestFile(require.resolve('./_field_data')); + loadTestFile(require.resolve('./classic/_field_data')); loadTestFile(require.resolve('./_field_data_with_fields_api')); + loadTestFile(require.resolve('./classic/_field_data_with_fields_api')); loadTestFile(require.resolve('./_shared_links')); loadTestFile(require.resolve('./_sidebar')); loadTestFile(require.resolve('./_source_filters')); loadTestFile(require.resolve('./_large_string')); loadTestFile(require.resolve('./_inspector')); - loadTestFile(require.resolve('./_classic_table_doc_navigation')); + loadTestFile(require.resolve('./classic/_classic_table_doc_navigation')); loadTestFile(require.resolve('./_date_nanos')); loadTestFile(require.resolve('./_date_nanos_mixed')); loadTestFile(require.resolve('./_indexpattern_without_timefield')); + loadTestFile(require.resolve('./classic/_discover_fields_api')); loadTestFile(require.resolve('./_discover_fields_api')); loadTestFile(require.resolve('./_data_grid')); loadTestFile(require.resolve('./_data_grid_context')); diff --git a/test/functional/apps/management/_scripted_fields.ts b/test/functional/apps/management/_scripted_fields.ts index a6bbe798cf56b..cefaa5b295369 100644 --- a/test/functional/apps/management/_scripted_fields.ts +++ b/test/functional/apps/management/_scripted_fields.ts @@ -32,6 +32,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); + const find = getService('find'); + const dataGrid = getService('dataGrid'); const PageObjects = getPageObjects([ 'common', 'header', @@ -48,7 +50,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.setWindowSize(1200, 800); await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); await kibanaServer.uiSettings.replace({}); - await kibanaServer.uiSettings.update({ 'doc_table:legacy': true }); }); after(async function afterAll() { @@ -56,6 +57,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.replace({}); }); + /** + * @param field field name to sort + * @param optionIndex index of the option to choose in dropdown + */ + const clickSort = async (field: string, optionIndex: number) => { + await testSubjects.click(`dataGridHeaderCell-${field}`); + const optionButtons = await find.allByCssSelector('.euiListGroupItem__button'); + await optionButtons[optionIndex].click(); + }; + it('should not allow saving of invalid scripts', async function () { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); @@ -146,29 +157,37 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async function () { - const rowData = await PageObjects.discover.getDocTableIndexLegacy(1); - expect(rowData).to.be('Sep 18, 2015 @ 18:20:57.916\n18'); + const rowData = (await dataGrid.getRowsText())[0]; + expect(rowData).to.be('Sep 18, 2015 @ 18:20:57.91618'); }); }); // add a test to sort numeric scripted field it('should sort scripted field value in Discover', async function () { - await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName}`); + await clickSort(scriptedPainlessFieldName, 1); + await PageObjects.common.sleep(500); + // after the first click on the scripted field, it becomes secondary sort after time. // click on the timestamp twice to make it be the secondary sort key. - await testSubjects.click('docTableHeaderFieldSort_@timestamp'); - await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await clickSort('@timestamp', 1); + await clickSort('@timestamp', 0); + await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async function () { - const rowData = await PageObjects.discover.getDocTableIndexLegacy(1); - expect(rowData).to.be('Sep 17, 2015 @ 10:53:14.181\n-1'); + const rowData = (await dataGrid.getRowsText())[0]; + expect(rowData).to.be('Sep 17, 2015 @ 10:53:14.181-1'); }); - await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName}`); + await clickSort(scriptedPainlessFieldName, 2); + // after the first click on the scripted field, it becomes primary sort after time. + // click on the scripted field twice then, makes it be the secondary sort key. + await clickSort(scriptedPainlessFieldName, 2); + await clickSort(scriptedPainlessFieldName, 2); + await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async function () { - const rowData = await PageObjects.discover.getDocTableIndexLegacy(1); - expect(rowData).to.be('Sep 17, 2015 @ 06:32:29.479\n20'); + const rowData = (await dataGrid.getRowsText())[0]; + expect(rowData).to.be('Sep 17, 2015 @ 06:32:29.47920'); }); }); @@ -233,29 +252,37 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async function () { - const rowData = await PageObjects.discover.getDocTableIndexLegacy(1); - expect(rowData).to.be('Sep 18, 2015 @ 18:20:57.916\ngood'); + const rowData = (await dataGrid.getRowsText())[0]; + expect(rowData).to.be('Sep 18, 2015 @ 18:20:57.916good'); }); }); // add a test to sort string scripted field it('should sort scripted field value in Discover', async function () { - await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); + await clickSort(scriptedPainlessFieldName2, 1); + // await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); + await PageObjects.common.sleep(500); + // after the first click on the scripted field, it becomes secondary sort after time. // click on the timestamp twice to make it be the secondary sort key. - await testSubjects.click('docTableHeaderFieldSort_@timestamp'); - await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await clickSort('@timestamp', 1); + await clickSort('@timestamp', 0); await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async function () { - const rowData = await PageObjects.discover.getDocTableIndexLegacy(1); - expect(rowData).to.be('Sep 17, 2015 @ 09:48:40.594\nbad'); + const rowData = (await dataGrid.getRowsText())[0]; + expect(rowData).to.be('Sep 17, 2015 @ 09:48:40.594bad'); }); - await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); + await clickSort(scriptedPainlessFieldName2, 2); + // after the first click on the scripted field, it becomes primary sort after time. + // click on the scripted field twice then, makes it be the secondary sort key. + await clickSort(scriptedPainlessFieldName2, 2); + await clickSort(scriptedPainlessFieldName2, 2); + await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async function () { - const rowData = await PageObjects.discover.getDocTableIndexLegacy(1); - expect(rowData).to.be('Sep 17, 2015 @ 06:32:29.479\ngood'); + const rowData = (await dataGrid.getRowsText())[0]; + expect(rowData).to.be('Sep 17, 2015 @ 06:32:29.479good'); }); }); @@ -319,8 +346,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async function () { - const rowData = await PageObjects.discover.getDocTableIndexLegacy(1); - expect(rowData).to.be('Sep 18, 2015 @ 18:20:57.916\ntrue'); + const rowData = (await dataGrid.getRowsText())[0]; + expect(rowData).to.be('Sep 18, 2015 @ 18:20:57.916true'); }); }); @@ -406,8 +433,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async function () { - const rowData = await PageObjects.discover.getDocTableIndexLegacy(1); - expect(rowData).to.be('Sep 18, 2015 @ 06:52:55.953\n2015-09-18 07:00'); + const rowData = (await dataGrid.getRowsText())[0]; + expect(rowData).to.be('Sep 18, 2015 @ 06:52:55.9532015-09-18 07:00'); }); }); diff --git a/test/functional/apps/management/_scripted_fields_classic_table.ts b/test/functional/apps/management/_scripted_fields_classic_table.ts new file mode 100644 index 0000000000000..a6bbe798cf56b --- /dev/null +++ b/test/functional/apps/management/_scripted_fields_classic_table.ts @@ -0,0 +1,459 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// Tests for 4 scripted fields; +// 1. Painless (number type) +// 2. Painless (string type) +// 3. Painless (boolean type) +// 4. Painless (date type) +// +// Each of these scripted fields has 4 tests (12 tests total); +// 1. Create scripted field +// 2. See the expected value of the scripted field in Discover doc view +// 3. Filter in Discover by the scripted field +// 4. Visualize with aggregation on the scripted field by clicking discover.clickFieldListItemVisualize + +// NOTE: Scripted field input is managed by Ace editor, which automatically +// appends closing braces, for exmaple, if you type opening square brace [ +// it will automatically insert a a closing square brace ], etc. + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); + const log = getService('log'); + const browser = getService('browser'); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + const filterBar = getService('filterBar'); + const PageObjects = getPageObjects([ + 'common', + 'header', + 'settings', + 'visualize', + 'discover', + 'timePicker', + ]); + + describe('scripted fields', function () { + this.tags(['skipFirefox']); + + before(async function () { + await browser.setWindowSize(1200, 800); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.uiSettings.replace({}); + await kibanaServer.uiSettings.update({ 'doc_table:legacy': true }); + }); + + after(async function afterAll() { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.uiSettings.replace({}); + }); + + it('should not allow saving of invalid scripts', async function () { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndexPatterns(); + await PageObjects.settings.clickIndexPatternLogstash(); + await PageObjects.settings.clickScriptedFieldsTab(); + await PageObjects.settings.clickAddScriptedField(); + await PageObjects.settings.setScriptedFieldName('doomedScriptedField'); + await PageObjects.settings.setScriptedFieldScript(`i n v a l i d s c r i p t`); + await PageObjects.settings.clickSaveScriptedField(); + await retry.try(async () => { + const invalidScriptErrorExists = await testSubjects.exists('invalidScriptError'); + expect(invalidScriptErrorExists).to.be(true); + }); + }); + + describe('testing regression for issue #33251', function describeIndexTests() { + const scriptedPainlessFieldName = 'ram_Pain_reg'; + + it('should create and edit scripted field', async function () { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndexPatterns(); + await PageObjects.settings.clickIndexPatternLogstash(); + const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10); + await PageObjects.settings.clickScriptedFieldsTab(); + await log.debug('add scripted field'); + const script = `1`; + await PageObjects.settings.addScriptedField( + scriptedPainlessFieldName, + 'painless', + 'number', + null, + '1', + script + ); + await retry.try(async function () { + expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be( + startingCount + 1 + ); + }); + + for (let i = 0; i < 3; i++) { + await PageObjects.settings.editScriptedField(scriptedPainlessFieldName); + const fieldSaveButton = await testSubjects.exists('fieldSaveButton'); + expect(fieldSaveButton).to.be(true); + await PageObjects.settings.clickSaveScriptedField(); + } + }); + }); + + describe('creating and using Painless numeric scripted fields', function describeIndexTests() { + const scriptedPainlessFieldName = 'ram_Pain1'; + + it('should create scripted field', async function () { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndexPatterns(); + await PageObjects.settings.clickIndexPatternLogstash(); + const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10); + await PageObjects.settings.clickScriptedFieldsTab(); + await log.debug('add scripted field'); + const script = `if (doc['machine.ram'].size() == 0) return -1; + else return doc['machine.ram'].value / (1024 * 1024 * 1024); + `; + await PageObjects.settings.addScriptedField( + scriptedPainlessFieldName, + 'painless', + 'number', + null, + '100', + script + ); + await retry.try(async function () { + expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be( + startingCount + 1 + ); + }); + }); + + it('should see scripted field value in Discover', async function () { + const fromTime = 'Sep 17, 2015 @ 06:31:44.000'; + const toTime = 'Sep 18, 2015 @ 18:31:44.000'; + await PageObjects.common.navigateToApp('discover'); + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + + await PageObjects.discover.clickFieldListItem(scriptedPainlessFieldName); + await retry.try(async function () { + await PageObjects.discover.clickFieldListItemAdd(scriptedPainlessFieldName); + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await retry.try(async function () { + const rowData = await PageObjects.discover.getDocTableIndexLegacy(1); + expect(rowData).to.be('Sep 18, 2015 @ 18:20:57.916\n18'); + }); + }); + + // add a test to sort numeric scripted field + it('should sort scripted field value in Discover', async function () { + await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName}`); + // after the first click on the scripted field, it becomes secondary sort after time. + // click on the timestamp twice to make it be the secondary sort key. + await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async function () { + const rowData = await PageObjects.discover.getDocTableIndexLegacy(1); + expect(rowData).to.be('Sep 17, 2015 @ 10:53:14.181\n-1'); + }); + + await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName}`); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async function () { + const rowData = await PageObjects.discover.getDocTableIndexLegacy(1); + expect(rowData).to.be('Sep 17, 2015 @ 06:32:29.479\n20'); + }); + }); + + it('should filter by scripted field value in Discover', async function () { + await PageObjects.discover.clickFieldListItem(scriptedPainlessFieldName); + await log.debug('filter by the first value (14) in the expanded scripted field list'); + await PageObjects.discover.clickFieldListPlusFilter(scriptedPainlessFieldName, '14'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await retry.try(async function () { + expect(await PageObjects.discover.getHitCount()).to.be('31'); + }); + }); + + it('should visualize scripted field in vertical bar chart', async function () { + await filterBar.removeAllFilters(); + await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName); + await PageObjects.header.waitUntilLoadingHasFinished(); + // verify Lens opens a visualization + expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain( + '@timestamp', + 'Median of ram_Pain1' + ); + }); + }); + + describe('creating and using Painless string scripted fields', function describeIndexTests() { + const scriptedPainlessFieldName2 = 'painString'; + + it('should create scripted field', async function () { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndexPatterns(); + await PageObjects.settings.clickIndexPatternLogstash(); + const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10); + await PageObjects.settings.clickScriptedFieldsTab(); + await log.debug('add scripted field'); + await PageObjects.settings.addScriptedField( + scriptedPainlessFieldName2, + 'painless', + 'string', + null, + '1', + "if (doc['response.raw'].value == '200') { return 'good'} else { return 'bad'}" + ); + await retry.try(async function () { + expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be( + startingCount + 1 + ); + }); + }); + + it('should see scripted field value in Discover', async function () { + const fromTime = 'Sep 17, 2015 @ 06:31:44.000'; + const toTime = 'Sep 18, 2015 @ 18:31:44.000'; + await PageObjects.common.navigateToApp('discover'); + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + + await PageObjects.discover.clickFieldListItem(scriptedPainlessFieldName2); + await retry.try(async function () { + await PageObjects.discover.clickFieldListItemAdd(scriptedPainlessFieldName2); + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await retry.try(async function () { + const rowData = await PageObjects.discover.getDocTableIndexLegacy(1); + expect(rowData).to.be('Sep 18, 2015 @ 18:20:57.916\ngood'); + }); + }); + + // add a test to sort string scripted field + it('should sort scripted field value in Discover', async function () { + await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); + // after the first click on the scripted field, it becomes secondary sort after time. + // click on the timestamp twice to make it be the secondary sort key. + await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async function () { + const rowData = await PageObjects.discover.getDocTableIndexLegacy(1); + expect(rowData).to.be('Sep 17, 2015 @ 09:48:40.594\nbad'); + }); + + await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async function () { + const rowData = await PageObjects.discover.getDocTableIndexLegacy(1); + expect(rowData).to.be('Sep 17, 2015 @ 06:32:29.479\ngood'); + }); + }); + + it('should filter by scripted field value in Discover', async function () { + await PageObjects.discover.clickFieldListItem(scriptedPainlessFieldName2); + await log.debug('filter by "bad" in the expanded scripted field list'); + await PageObjects.discover.clickFieldListPlusFilter(scriptedPainlessFieldName2, 'bad'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await retry.try(async function () { + expect(await PageObjects.discover.getHitCount()).to.be('27'); + }); + await filterBar.removeAllFilters(); + }); + + it('should visualize scripted field in vertical bar chart', async function () { + await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName2); + await PageObjects.header.waitUntilLoadingHasFinished(); + // verify Lens opens a visualization + expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain( + 'Top 5 values of painString' + ); + }); + }); + + describe('creating and using Painless boolean scripted fields', function describeIndexTests() { + const scriptedPainlessFieldName2 = 'painBool'; + + it('should create scripted field', async function () { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndexPatterns(); + await PageObjects.settings.clickIndexPatternLogstash(); + const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10); + await PageObjects.settings.clickScriptedFieldsTab(); + await log.debug('add scripted field'); + await PageObjects.settings.addScriptedField( + scriptedPainlessFieldName2, + 'painless', + 'boolean', + null, + '1', + "doc['response.raw'].value == '200'" + ); + await retry.try(async function () { + expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be( + startingCount + 1 + ); + }); + }); + + it('should see scripted field value in Discover', async function () { + const fromTime = 'Sep 17, 2015 @ 06:31:44.000'; + const toTime = 'Sep 18, 2015 @ 18:31:44.000'; + await PageObjects.common.navigateToApp('discover'); + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + + await PageObjects.discover.clickFieldListItem(scriptedPainlessFieldName2); + await retry.try(async function () { + await PageObjects.discover.clickFieldListItemAdd(scriptedPainlessFieldName2); + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await retry.try(async function () { + const rowData = await PageObjects.discover.getDocTableIndexLegacy(1); + expect(rowData).to.be('Sep 18, 2015 @ 18:20:57.916\ntrue'); + }); + }); + + it('should filter by scripted field value in Discover', async function () { + await PageObjects.discover.clickFieldListItem(scriptedPainlessFieldName2); + await log.debug('filter by "true" in the expanded scripted field list'); + await PageObjects.discover.clickFieldListPlusFilter(scriptedPainlessFieldName2, 'true'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await retry.try(async function () { + expect(await PageObjects.discover.getHitCount()).to.be('359'); + }); + await filterBar.removeAllFilters(); + }); + + // add a test to sort boolean + // existing bug: https://github.com/elastic/kibana/issues/75519 hence the issue is skipped. + it.skip('should sort scripted field value in Discover', async function () { + await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); + // after the first click on the scripted field, it becomes secondary sort after time. + // click on the timestamp twice to make it be the secondary sort key. + await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async function () { + const rowData = await PageObjects.discover.getDocTableIndexLegacy(1); + expect(rowData).to.be('updateExpectedResultHere\ntrue'); + }); + + await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async function () { + const rowData = await PageObjects.discover.getDocTableIndexLegacy(1); + expect(rowData).to.be('updateExpectedResultHere\nfalse'); + }); + }); + + it('should visualize scripted field in vertical bar chart', async function () { + await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName2); + await PageObjects.header.waitUntilLoadingHasFinished(); + // verify Lens opens a visualization + expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain( + 'Top 5 values of painBool' + ); + }); + }); + + describe('creating and using Painless date scripted fields', function describeIndexTests() { + const scriptedPainlessFieldName2 = 'painDate'; + + it('should create scripted field', async function () { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndexPatterns(); + await PageObjects.settings.clickIndexPatternLogstash(); + const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10); + await PageObjects.settings.clickScriptedFieldsTab(); + await log.debug('add scripted field'); + await PageObjects.settings.addScriptedField( + scriptedPainlessFieldName2, + 'painless', + 'date', + { format: 'date', datePattern: 'YYYY-MM-DD HH:00' }, + '1', + "doc['utc_time'].value.toEpochMilli() + (1000) * 60 * 60" + ); + await retry.try(async function () { + expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be( + startingCount + 1 + ); + }); + }); + + it('should see scripted field value in Discover', async function () { + const fromTime = 'Sep 17, 2015 @ 19:22:00.000'; + const toTime = 'Sep 18, 2015 @ 07:00:00.000'; + await PageObjects.common.navigateToApp('discover'); + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + + await PageObjects.discover.clickFieldListItem(scriptedPainlessFieldName2); + await retry.try(async function () { + await PageObjects.discover.clickFieldListItemAdd(scriptedPainlessFieldName2); + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await retry.try(async function () { + const rowData = await PageObjects.discover.getDocTableIndexLegacy(1); + expect(rowData).to.be('Sep 18, 2015 @ 06:52:55.953\n2015-09-18 07:00'); + }); + }); + + // add a test to sort date scripted field + // https://github.com/elastic/kibana/issues/75711 + it.skip('should sort scripted field value in Discover', async function () { + await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); + // after the first click on the scripted field, it becomes secondary sort after time. + // click on the timestamp twice to make it be the secondary sort key. + await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async function () { + const rowData = await PageObjects.discover.getDocTableIndexLegacy(1); + expect(rowData).to.be('updateExpectedResultHere\n2015-09-18 07:00'); + }); + + await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.try(async function () { + const rowData = await PageObjects.discover.getDocTableIndexLegacy(1); + expect(rowData).to.be('updateExpectedResultHere\n2015-09-18 07:00'); + }); + }); + + it('should filter by scripted field value in Discover', async function () { + await PageObjects.discover.clickFieldListItem(scriptedPainlessFieldName2); + await log.debug('filter by "Sep 17, 2015 @ 23:00" in the expanded scripted field list'); + await PageObjects.discover.clickFieldListPlusFilter( + scriptedPainlessFieldName2, + '1442531297065' + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await retry.try(async function () { + expect(await PageObjects.discover.getHitCount()).to.be('1'); + }); + await filterBar.removeAllFilters(); + }); + + it('should visualize scripted field in vertical bar chart', async function () { + await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName2); + await PageObjects.header.waitUntilLoadingHasFinished(); + // verify Lens opens a visualization + expect(await testSubjects.getVisibleTextAll('lns-dimensionTrigger')).to.contain('painDate'); + }); + }); + }); +} diff --git a/test/functional/apps/management/index.ts b/test/functional/apps/management/index.ts index 09f9001d0236a..48cb90393d372 100644 --- a/test/functional/apps/management/index.ts +++ b/test/functional/apps/management/index.ts @@ -30,6 +30,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_mgmt_import_saved_objects')); loadTestFile(require.resolve('./_index_patterns_empty')); loadTestFile(require.resolve('./_scripted_fields')); + loadTestFile(require.resolve('./_scripted_fields_classic_table')); loadTestFile(require.resolve('./_runtime_fields')); loadTestFile(require.resolve('./_field_formatter')); loadTestFile(require.resolve('./_legacy_url_redirect')); diff --git a/x-pack/test/functional/apps/discover/value_suggestions.ts b/x-pack/test/functional/apps/discover/value_suggestions.ts index bb5a9098bf901..3151f729aa58c 100644 --- a/x-pack/test/functional/apps/discover/value_suggestions.ts +++ b/x-pack/test/functional/apps/discover/value_suggestions.ts @@ -13,7 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const queryBar = getService('queryBar'); const filterBar = getService('filterBar'); - const docTable = getService('docTable'); + const dataGrid = getService('dataGrid'); const PageObjects = getPageObjects(['common', 'timePicker', 'settings', 'context']); async function setAutocompleteUseTimeRange(value: boolean) { @@ -34,7 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'x-pack/test/functional/fixtures/kbn_archiver/dashboard_drilldowns/drilldowns' ); await kibanaServer.uiSettings.update({ - 'doc_table:legacy': true, + 'doc_table:legacy': false, }); }); @@ -90,9 +90,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); // navigate to context - await docTable.clickRowToggle({ rowIndex: 0 }); - const rowActions = await docTable.getRowActions({ rowIndex: 0 }); - await rowActions[0].click(); + await dataGrid.clickRowToggle({ rowIndex: 0 }); + const rowActions = await dataGrid.getRowActions({ rowIndex: 0 }); + await rowActions[1].click(); await PageObjects.context.waitUntilContextLoadingHasFinished(); // Apply filter in context view diff --git a/x-pack/test/functional/apps/discover/value_suggestions_non_timebased.ts b/x-pack/test/functional/apps/discover/value_suggestions_non_timebased.ts index 8d95d85a88e1e..7d8f8a302b05c 100644 --- a/x-pack/test/functional/apps/discover/value_suggestions_non_timebased.ts +++ b/x-pack/test/functional/apps/discover/value_suggestions_non_timebased.ts @@ -24,7 +24,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); await kibanaServer.uiSettings.replace({ defaultIndex: 'without-timefield' }); await kibanaServer.uiSettings.update({ - 'doc_table:legacy': true, + 'doc_table:legacy': false, }); }); From ecc1ec993d7cd33eed285e873c61e7cabd00a0ba Mon Sep 17 00:00:00 2001 From: Mat Schaffer Date: Thu, 23 Jun 2022 19:03:42 +0900 Subject: [PATCH 18/54] [Stack Monitoring] Remove unexpected docs from persistentMetricsetsQuery portion of health API response (#134976) --- .../monitored_clusters_query.ts | 66 ++++++++++++------- 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/monitored_clusters_query.ts b/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/monitored_clusters_query.ts index 825cabc723294..5baf6e11c4082 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/monitored_clusters_query.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/_health/monitored_clusters/monitored_clusters_query.ts @@ -56,11 +56,37 @@ export const monitoredClustersQuery = ({ timeRange, timeout }: QueryOptions) => }; /** - * some metricset documents use a stable ID to maintain a single occurence of + * some metricset documents use a stable ID to maintain a single occurrence of * the documents in the index. we query those metricsets separately without * a time range filter */ export const persistentMetricsetsQuery = ({ timeout }: QueryOptions) => { + const shardMatches = [ + { + term: { + type: 'shards', + }, + }, + { + term: { + 'metricset.name': 'shard', + }, + }, + ]; + + const logstashStateMatches = [ + { + term: { + type: 'logstash_state', + }, + }, + { + term: { + 'metricset.name': 'node', + }, + }, + ]; + const metricsetsAggregations = { elasticsearch: { terms: { @@ -71,18 +97,7 @@ export const persistentMetricsetsQuery = ({ timeout }: QueryOptions) => { shard: lastSeenByIndex({ filter: { bool: { - should: [ - { - term: { - type: 'shards', - }, - }, - { - term: { - 'metricset.name': 'shard', - }, - }, - ], + should: shardMatches, }, }, }), @@ -98,18 +113,7 @@ export const persistentMetricsetsQuery = ({ timeout }: QueryOptions) => { node: lastSeenByIndex({ filter: { bool: { - should: [ - { - term: { - type: 'logstash_state', - }, - }, - { - term: { - 'metricset.name': 'node', - }, - }, - ], + should: logstashStateMatches, }, }, }), @@ -117,8 +121,20 @@ export const persistentMetricsetsQuery = ({ timeout }: QueryOptions) => { }, }; + // Outer query on expected types to avoid catching kibana internal collection documents containing source_node.uuid return { timeout: `${timeout}s`, + query: { + bool: { + filter: [ + { + bool: { + should: shardMatches.concat(logstashStateMatches), + }, + }, + ], + }, + }, aggs: { clusters: { terms: clusterUuidTerm, From c82788c4bb61134d40abc69fe6dc4b5b08b74868 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Thu, 23 Jun 2022 07:19:13 -0400 Subject: [PATCH 19/54] Fixing bug and writing tests (#134945) --- .../saved_object/timelines/index.test.ts | 59 +++++++++++++++++++ .../timeline/saved_object/timelines/index.ts | 4 +- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.test.ts index 920d9a85ed484..c2db0e00a1400 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.test.ts @@ -13,6 +13,7 @@ import { getExistingPrepackagedTimelines, getAllTimeline, resolveTimelineOrNull, + updatePartialSavedTimeline, } from '.'; import { convertSavedObjectToSavedTimeline } from './convert_saved_object_to_savedtimeline'; import { getNotesByTimelineId } from '../notes/saved_object'; @@ -20,6 +21,7 @@ import { getAllPinnedEventsByTimelineId } from '../pinned_events'; import { AllTimelinesResponse, ResolvedTimelineWithOutcomeSavedObject, + SavedTimeline, } from '../../../../../common/types/timeline'; import { mockResolvedSavedObject, @@ -28,6 +30,7 @@ import { } from '../../__mocks__/resolve_timeline'; import { DATA_VIEW_ID_REF_NAME, SAVED_QUERY_ID_REF_NAME, SAVED_QUERY_TYPE } from '../../constants'; import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common'; +import { SavedObjectsUpdateResponse } from '@kbn/core/server'; jest.mock('./convert_saved_object_to_savedtimeline', () => ({ convertSavedObjectToSavedTimeline: jest.fn(), @@ -360,4 +363,60 @@ describe('saved_object', () => { expect(attributes.savedQueryId).toEqual('boo'); }); }); + + describe('updatePartialSavedTimeline', () => { + let mockSOClientGet: jest.Mock; + let mockSOClientUpdate: jest.Mock; + let mockRequest: FrameworkRequest; + + const patchTimelineRequest: SavedTimeline = { + savedQueryId: null, + }; + + beforeEach(() => { + jest.clearAllMocks(); + + mockSOClientUpdate = jest.fn(() => ({ + ...mockResolvedSavedObject.saved_object, + attributes: {}, + })); + + mockSOClientGet = jest.fn(async () => ({ + ...mockResolvedSavedObject.saved_object, + references: [ + { + id: 'boo', + name: SAVED_QUERY_ID_REF_NAME, + type: SAVED_QUERY_TYPE, + }, + ], + })); + + mockRequest = { + user: { + username: 'username', + }, + context: { + core: { + savedObjects: { + client: { + get: mockSOClientGet, + update: mockSOClientUpdate, + }, + }, + }, + }, + } as unknown as FrameworkRequest; + }); + + it('does not remove savedQueryId when it is null in the patch request', async () => { + const resp = (await updatePartialSavedTimeline( + mockRequest, + '760d3d20-2142-11ec-a46f-051cb8e3154c', + patchTimelineRequest + )) as SavedObjectsUpdateResponse; + + expect(resp.attributes.savedQueryId).toBeNull(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.ts index de7c31fb47a98..03b16c3292b7e 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.ts @@ -493,7 +493,7 @@ const updateTimeline = async ({ }; }; -const updatePartialSavedTimeline = async ( +export const updatePartialSavedTimeline = async ( request: FrameworkRequest, timelineId: string, timeline: SavedTimeline @@ -528,7 +528,7 @@ const updatePartialSavedTimeline = async ( const populatedTimeline = timelineFieldsMigrator.populateFieldsFromReferencesForPatch({ - dataBeforeRequest: timelineUpdateAttributes, + dataBeforeRequest: timeline, dataReturnedFromRequest: updatedTimeline, }); From bee025b3c09f913acddf2eb116b3d1c06f7e5926 Mon Sep 17 00:00:00 2001 From: Paul Power <91835463+pauldotpower@users.noreply.github.com> Date: Thu, 23 Jun 2022 12:21:30 +0100 Subject: [PATCH 20/54] Changed a minor error - "to to" to "to" (#126760) A minor change, not urgent/important Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/setup/upgrade/resolving-migration-failures.asciidoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/setup/upgrade/resolving-migration-failures.asciidoc b/docs/setup/upgrade/resolving-migration-failures.asciidoc index 7ba4bc7205fcf..85847b15cb084 100644 --- a/docs/setup/upgrade/resolving-migration-failures.asciidoc +++ b/docs/setup/upgrade/resolving-migration-failures.asciidoc @@ -8,9 +8,9 @@ with the new version. ==== Saved object migration failures If {kib} unexpectedly terminates while migrating a saved object index, {kib} automatically attempts to -perform the migration again when the process restarts. Do not delete any saved objects indices to -to fix a failed migration. Unlike previous versions, {kib} 7.12.0 and -later does not require deleting indices to release a failed migration lock. +perform the migration again when the process restarts. Do not delete any saved objects indices to +fix a failed migration. Unlike previous versions, {kib} 7.12.0 and later does not require deleting +indices to release a failed migration lock. If upgrade migrations fail repeatedly, refer to <>. From 8e1feb327a434db25a4157a74170a5015f3dcab3 Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Thu, 23 Jun 2022 13:33:39 +0200 Subject: [PATCH 21/54] [Enterprise Search]Add util function to create Kea logic files for API calls (#134932) * [Enterprise Search]Add util function to create Kea logic files for API calls * Fixed unit tests for add custom source logic * Make Status an enum, add tests, move flash messages * Fix some more tests --- x-pack/plugins/enterprise_search/KEA.md | 152 +++++------------- .../enterprise_search/common/types/api.ts | 33 +++- .../shared/api_logic/create_api_logic.test.ts | 130 +++++++++++++++ .../shared/api_logic/create_api_logic.ts | 76 +++++++++ .../add_custom_source_api_logic.test.ts | 152 +++--------------- .../add_custom_source_api_logic.ts | 93 +++-------- .../add_custom_source_logic.test.ts | 102 ++++++++---- .../add_custom_source_logic.ts | 34 ++-- 8 files changed, 404 insertions(+), 368 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/api_logic/create_api_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/api_logic/create_api_logic.ts diff --git a/x-pack/plugins/enterprise_search/KEA.md b/x-pack/plugins/enterprise_search/KEA.md index 17ae7cf71f833..368dc7d13ea73 100644 --- a/x-pack/plugins/enterprise_search/KEA.md +++ b/x-pack/plugins/enterprise_search/KEA.md @@ -26,99 +26,42 @@ Slicing up components into smaller chunks, designing clear interfaces for those State management tools are most powerful when used to coordinate state across an entire application, or large slices of that application. To do that well, state needs to be shared and it needs to be clear where in the existing state to find what information. We do this by separating API data from component data. -This means API interactions and their data should live in their own logic files, and the resulting data and API status should be imported by other logic files or directly by components consuming that data. Those API logic files should contain all interactions with APIs, and the current status of those API requests. Our idiomatic way of doing this follows: +This means API interactions and their data should live in their own logic files, and the resulting data and API status should be imported by other logic files or directly by components consuming that data. Those API logic files should contain all interactions with APIs, and the current status of those API requests. We have a util function to help you create those, located in [create_api_logic.ts](public/applications/shared/api_logic/create_api_logic.ts). You can grab the `status`, `data` and `error` values from any API created with that util. And you can listen to the `initiateCall`, `apiSuccess`, `apiError` and `apiReset` actions in other listeners. -```typescript -import { kea, MakeLogicType } from 'kea'; - -import { ApiStatus, HttpError } from '../../../../../../../../common/types/api'; -import { flashAPIErrors, clearFlashMessages } from '../../../../../../shared/flash_messages'; -import { HttpLogic } from '../../../../../../shared/http'; +You will need to provide a function that actually makes the api call, as well as the logic path. The function will need to accept and return a single object, not separate values. -export interface AddCustomSourceActions { - fetchSource(): void; - fetchSourceError(code: number, error: string): HttpError; - fetchSourceSuccess(source: CustomSource): CustomSource; -} - -interface CustomSource { +```typescript +export const addCustomSource = async ({ + name, + baseServiceType, +}: { name: string; -} - -interface AddCustomSourceValues { - sourceApiStatus: ApiStatus; -} - -export const AddCustomSourceLogic = kea< - MakeLogicType ->({ - path: ['enterprise_search', 'workplace_search', 'add_custom_source_logic'], - actions: { - fetchSource: true, - fetchSourceError: (code, error) => ({ code, error }), - fetchSourceSuccess: (customSource) => customSource, - }, - reducers: () => ({ - sourceApiStatus: [ - { - status: 'IDLE', - }, - { - fetchSource: () => ({ - status: 'PENDING', - }), - fetchSourceError: (_, error) => ({ - status: 'ERROR', - error, - }), - fetchSourceSuccess: (_, data) => ({ - status: 'SUCCESS', - data, - }), - }, - ], - }), - listeners: ({ actions }) => ({ - fetchSource: async () => { - clearFlashMessages(); - - try { - const response = await HttpLogic.values.http.post('/api/source'); - actions.fetchSourceSuccess(response); - } catch (e) { - flashAPIErrors(e); - actions.fetchSourceError(e.code, e.message); - } - }, - }), -}); + baseServiceType?: string; +}) => { + const { isOrganization } = AppLogic.values; + const route = isOrganization + ? '/internal/workplace_search/org/create_source' + : '/internal/workplace_search/account/create_source'; + + const params = { + service_type: 'custom', + name, + base_service_type: baseServiceType, + }; + const source = await HttpLogic.values.http.post(route, { + body: JSON.stringify(params), + }); + return { source }; +}; + +export const AddCustomSourceApiLogic = createApiLogic( + ['add_custom_source_api_logic'], + addCustomSource +); ``` -The types used above can be found in our [common Enterprise Search types file](common/types/api.ts). While the above assumes a single, idempotent API, this approach can be easily extended to use a dictionary approach: -```typescript -reducers: () => ({ - sourceApiStatus: [ - { - }, - { - fetchSource: (state, id) => ({...state, - id: { - status: 'PENDING', - data: state[id]?.data, - }}), - fetchSourceError: (_, ({id, error})) => ({...state, - id: { - status: 'ERROR', - error, - }}), - fetchSourceSuccess: (_, ({id, data})) => ({...state, id: { - status: 'SUCCESS', - data, - }}), - }, - ], - }), -``` +The types used in that util can be found in our [common Enterprise Search types file](common/types/api.ts). + ## Import actions and values from API logic files into component and view logic. Once you have an API interactions file set up, components and other Kea logic files can import the values from those files to build their own logic. Use the Kea 'connect' functionality to do this, as the auto-connect functionality has a few bugs and was removed in Kea 3.0. This allows you to read the status and value of an API, react to any API events, and abstract those APIs away from the components. Those components can now become more functional and reactive to the current state of the application, rather than to directly responding to API events. @@ -130,34 +73,19 @@ export const AddCustomSourceLogic = kea< MakeLogicType >({ connect: { - actions: [AddCustomSourceApiLogic, ['addSource', 'addSourceSuccess', 'addSourceError']], - values: [AddCustomSourceApiLogic, ['sourceApi']], + actions: [AddCustomSourceApiLogic, ['initiateCall', 'apiSuccess', ]], + values: [AddCustomSourceApiLogic, ['status']], }, path: ['enterprise_search', 'workplace_search', 'add_custom_source_logic'], actions: { createContentSource: true, - setCustomSourceNameValue: (customSourceNameValue) => customSourceNameValue, setNewCustomSource: (data) => data, }, - reducers: ({ props }) => ({ - customSourceNameValue: [ - props.initialValue || '', - { - setCustomSourceNameValue: (_, customSourceNameValue) => customSourceNameValue, - }, - ], - newCustomSource: [ - undefined, - { - setNewCustomSource: (_, newCustomSource) => newCustomSource, - }, - ], - }), listeners: ({ actions, values, props }) => ({ createContentSource: () => { const { customSourceNameValue } = values; const { baseServiceType } = props; - actions.addSource(customSourceNameValue, baseServiceType); + actions.initiateCall({ source: customSourceNameValue, baseServiceType }); }, addSourceSuccess: (customSource: CustomSource) => { actions.setNewCustomSource(customSource); @@ -165,20 +93,14 @@ export const AddCustomSourceLogic = kea< }), selectors: { buttonLoading: [ - (selectors) => [selectors.sourceApi], - (apiStatus) => apiStatus?.status === 'PENDING', + (selectors) => [selectors.status], + (apiStatus) => status === 'LOADING', ], }, }); ``` -You'll have to add the imported the actions and values types you're already using for your function, preferably by importing the types off the imported logic. Like so: -```typescript -export interface AddCustomSourceActions { - addSource: AddCustomSourceApiActions['addSource']; - addSourceSuccess: AddCustomSourceApiActions['addSourceSuccess']; -} -``` +You'll have to add the imported the actions and values types you're already using for your function, preferably by importing the types off the imported logic, so TypeScript can warn you if you're misusing the function. ## Keep your logic files small Using the above methods, you can keep your logic files small and isolated. Keep API calls separate from view and component logic. Keep the amount of logic you're processing limited per file. If your logic file starts exceeding about 150 lines of code, you should start thinking about splitting it up into separate chunks, if possible. diff --git a/x-pack/plugins/enterprise_search/common/types/api.ts b/x-pack/plugins/enterprise_search/common/types/api.ts index 2ba225bd19223..579f9535c7e4b 100644 --- a/x-pack/plugins/enterprise_search/common/types/api.ts +++ b/x-pack/plugins/enterprise_search/common/types/api.ts @@ -5,36 +5,53 @@ * 2.0. */ +import { HttpResponse } from '@kbn/core/public'; + /** * These types track an API call's status and result * Each Status string corresponds to a possible status in a request's lifecycle */ -export type Status = 'IDLE' | 'PENDING' | 'SUCCESS' | 'ERROR'; +export const enum Status { + IDLE, + LOADING, + SUCCESS, + ERROR, +} -export interface HttpError { - code: number; - message?: string; +export interface ErrorResponse { + statusCode: number; + error: string; + message: string; + attributes: { + errors: string[]; + }; } +export type HttpError = HttpResponse; + export interface ApiSuccess { - status: 'SUCCESS'; + status: Status.SUCCESS; data: T; + error?: undefined; } export interface ApiPending { - status: 'PENDING'; + status: Status.LOADING; data?: T; + error?: undefined; } export interface ApiIdle { - status: 'IDLE'; + status: Status.IDLE; data?: T; + error?: undefined; } export interface ApiError { - status: Status; + status: Status.ERROR; error: HttpError; + data?: undefined; } export type ApiStatus = ApiSuccess | ApiPending | ApiIdle | ApiError; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/api_logic/create_api_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/api_logic/create_api_logic.test.ts new file mode 100644 index 0000000000000..3687ccbde7723 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/api_logic/create_api_logic.test.ts @@ -0,0 +1,130 @@ +/* + * 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 { LogicMounter } from '../../__mocks__/kea_logic'; + +import { nextTick } from '@kbn/test-jest-helpers'; + +import { HttpError, Status } from '../../../../common/types/api'; + +import { createApiLogic } from './create_api_logic'; + +const DEFAULT_VALUES = { + apiStatus: { + status: Status.IDLE, + }, + data: undefined, + error: undefined, + status: Status.IDLE, +}; + +describe('CreateApiLogic', () => { + const apiCallMock = jest.fn(); + const logic = createApiLogic(['path'], apiCallMock); + const { mount } = new LogicMounter(logic); + + beforeEach(() => { + jest.clearAllMocks(); + mount({}); + }); + + it('has expected default values', () => { + expect(logic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('makeRequest', () => { + it('should set status to LOADING', () => { + logic.actions.makeRequest({}); + expect(logic.values).toEqual({ + ...DEFAULT_VALUES, + status: Status.LOADING, + apiStatus: { + status: Status.LOADING, + }, + }); + }); + }); + describe('apiSuccess', () => { + it('should set status to SUCCESS and load data', () => { + logic.actions.apiSuccess({ success: 'data' }); + expect(logic.values).toEqual({ + ...DEFAULT_VALUES, + status: Status.SUCCESS, + data: { success: 'data' }, + apiStatus: { + status: Status.SUCCESS, + data: { success: 'data' }, + }, + }); + }); + }); + describe('apiError', () => { + it('should set status to ERROR and set error data', () => { + logic.actions.apiError('error' as any as HttpError); + expect(logic.values).toEqual({ + ...DEFAULT_VALUES, + status: Status.ERROR, + data: undefined, + error: 'error', + apiStatus: { + status: Status.ERROR, + data: undefined, + error: 'error', + }, + }); + }); + }); + describe('apiReset', () => { + it('should reset api', () => { + logic.actions.apiError('error' as any as HttpError); + expect(logic.values).toEqual({ + ...DEFAULT_VALUES, + status: Status.ERROR, + data: undefined, + error: 'error', + apiStatus: { + status: Status.ERROR, + data: undefined, + error: 'error', + }, + }); + logic.actions.apiReset(); + expect(logic.values).toEqual(DEFAULT_VALUES); + }); + }); + }); + + describe('listeners', () => { + describe('makeRequest', () => { + it('calls apiCall on success', async () => { + const apiSuccessMock = jest.spyOn(logic.actions, 'apiSuccess'); + const apiErrorMock = jest.spyOn(logic.actions, 'apiError'); + apiCallMock.mockReturnValue(Promise.resolve('result')); + logic.actions.makeRequest({ arg: 'argument1' }); + expect(apiCallMock).toHaveBeenCalledWith({ arg: 'argument1' }); + await nextTick(); + expect(apiErrorMock).not.toHaveBeenCalled(); + expect(apiSuccessMock).toHaveBeenCalledWith('result'); + }); + it('calls apiError on error', async () => { + const apiSuccessMock = jest.spyOn(logic.actions, 'apiSuccess'); + const apiErrorMock = jest.spyOn(logic.actions, 'apiError'); + apiCallMock.mockReturnValue( + Promise.reject({ body: { statusCode: 404, message: 'message' } }) + ); + logic.actions.makeRequest({ arg: 'argument1' }); + expect(apiCallMock).toHaveBeenCalledWith({ arg: 'argument1' }); + await nextTick(); + expect(apiSuccessMock).not.toHaveBeenCalled(); + expect(apiErrorMock).toHaveBeenCalledWith({ + body: { statusCode: 404, message: 'message' }, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/api_logic/create_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/api_logic/create_api_logic.ts new file mode 100644 index 0000000000000..57a8d3751a6ac --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/api_logic/create_api_logic.ts @@ -0,0 +1,76 @@ +/* + * 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 { kea, MakeLogicType } from 'kea'; + +import { ApiStatus, Status, HttpError } from '../../../../common/types/api'; + +export interface Values { + apiStatus: ApiStatus; + status: Status; + data?: T; + error: HttpError; +} + +export interface Actions | undefined, Result> { + makeRequest(args: Args): Args; + apiError(error: HttpError): HttpError; + apiSuccess(result: Result): Result; + apiReset(): void; +} + +export const createApiLogic = | undefined>( + path: string[], + apiFunction: (args: Args) => Promise +) => + kea, Actions>>({ + path: ['enterprise_search', ...path], + actions: { + makeRequest: (args) => args, + apiError: (error) => error, + apiSuccess: (result) => result, + apiReset: true, + }, + reducers: () => ({ + apiStatus: [ + { + status: Status.IDLE, + }, + { + makeRequest: () => ({ + status: Status.LOADING, + }), + apiError: (_, error) => ({ + status: Status.ERROR, + error, + }), + apiSuccess: (_, data) => ({ + status: Status.SUCCESS, + data, + }), + apiReset: () => ({ + status: Status.IDLE, + }), + }, + ], + }), + listeners: ({ actions }) => ({ + makeRequest: async (args) => { + try { + const result = await apiFunction(args); + actions.apiSuccess(result); + } catch (e) { + actions.apiError(e); + } + }, + }), + selectors: ({ selectors }) => ({ + status: [() => [selectors.apiStatus], (apiStatus: ApiStatus) => apiStatus.status], + data: [() => [selectors.apiStatus], (apiStatus: ApiStatus) => apiStatus.data], + error: [() => [selectors.apiStatus], (apiStatus: ApiStatus) => apiStatus.error], + }), + }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_api_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_api_logic.test.ts index a08d024e465ad..3f95f5058b0c2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_api_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_api_logic.test.ts @@ -5,151 +5,45 @@ * 2.0. */ -import { - LogicMounter, - mockFlashMessageHelpers, - mockHttpValues, -} from '../../../../../../__mocks__/kea_logic'; -import { sourceConfigData } from '../../../../../__mocks__/content_sources.mock'; +import { mockHttpValues } from '../../../../../../__mocks__/kea_logic'; import { nextTick } from '@kbn/test-jest-helpers'; -import { itShowsServerErrorAsFlashMessage } from '../../../../../../test_helpers'; - jest.mock('../../../../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); import { AppLogic } from '../../../../../app_logic'; -import { AddCustomSourceApiLogic } from './add_custom_source_api_logic'; - -const DEFAULT_VALUES = { - sourceApi: { - status: 'IDLE', - }, -}; - -const MOCK_NAME = 'name'; +import { addCustomSource } from './add_custom_source_api_logic'; -describe('AddCustomSourceLogic', () => { - const { mount } = new LogicMounter(AddCustomSourceApiLogic); +describe('addCustomSource', () => { const { http } = mockHttpValues; - const { clearFlashMessages } = mockFlashMessageHelpers; beforeEach(() => { jest.clearAllMocks(); - mount({}); }); - it('has expected default values', () => { - expect(AddCustomSourceApiLogic.values).toEqual(DEFAULT_VALUES); - }); - - describe('listeners', () => { - beforeEach(() => { - mount(); - }); - - describe('organization context', () => { - describe('createContentSource', () => { - it('calls API and sets values', async () => { - const addSourceSuccessSpy = jest.spyOn( - AddCustomSourceApiLogic.actions, - 'addSourceSuccess' - ); - http.post.mockReturnValue(Promise.resolve({ sourceConfigData })); - - AddCustomSourceApiLogic.actions.addSource(MOCK_NAME); - - expect(clearFlashMessages).toHaveBeenCalled(); - expect(http.post).toHaveBeenCalledWith('/internal/workplace_search/org/create_source', { - body: JSON.stringify({ service_type: 'custom', name: MOCK_NAME }), - }); - await nextTick(); - expect(addSourceSuccessSpy).toHaveBeenCalledWith({ sourceConfigData }); - }); - - it('submits a base service type for pre-configured sources', async () => { - const addSourceSuccessSpy = jest.spyOn( - AddCustomSourceApiLogic.actions, - 'addSourceSuccess' - ); - http.post.mockReturnValue(Promise.resolve({ sourceConfigData })); - - AddCustomSourceApiLogic.actions.addSource(MOCK_NAME, 'base_service_type'); - - expect(clearFlashMessages).toHaveBeenCalled(); - expect(http.post).toHaveBeenCalledWith('/internal/workplace_search/org/create_source', { - body: JSON.stringify({ - service_type: 'custom', - name: MOCK_NAME, - base_service_type: 'base_service_type', - }), - }); - await nextTick(); - expect(addSourceSuccessSpy).toHaveBeenCalledWith({ sourceConfigData }); - }); - - itShowsServerErrorAsFlashMessage(http.post, () => { - AddCustomSourceApiLogic.actions.addSource(MOCK_NAME); - }); - }); + it('calls correct route for organization', async () => { + const promise = Promise.resolve('result'); + http.post.mockReturnValue(promise); + addCustomSource({ name: 'name', baseServiceType: 'baseServiceType' }); + await nextTick(); + expect(http.post).toHaveBeenCalledWith('/internal/workplace_search/org/create_source', { + body: JSON.stringify({ + service_type: 'custom', + name: 'name', + base_service_type: 'baseServiceType', + }), }); - - describe('account context routes', () => { - beforeEach(() => { - AppLogic.values.isOrganization = false; - }); - - describe('createContentSource', () => { - it('calls API and sets values', async () => { - const addSourceSuccessSpy = jest.spyOn( - AddCustomSourceApiLogic.actions, - 'addSourceSuccess' - ); - http.post.mockReturnValue(Promise.resolve({ sourceConfigData })); - - AddCustomSourceApiLogic.actions.addSource(MOCK_NAME); - - expect(clearFlashMessages).toHaveBeenCalled(); - expect(http.post).toHaveBeenCalledWith( - '/internal/workplace_search/account/create_source', - { - body: JSON.stringify({ service_type: 'custom', name: MOCK_NAME }), - } - ); - await nextTick(); - expect(addSourceSuccessSpy).toHaveBeenCalledWith({ sourceConfigData }); - }); - - it('submits a base service type for pre-configured sources', async () => { - const addSourceSuccessSpy = jest.spyOn( - AddCustomSourceApiLogic.actions, - 'addSourceSuccess' - ); - http.post.mockReturnValue(Promise.resolve({ sourceConfigData })); - - AddCustomSourceApiLogic.actions.addSource(MOCK_NAME, 'base_service_type'); - - expect(clearFlashMessages).toHaveBeenCalled(); - expect(http.post).toHaveBeenCalledWith( - '/internal/workplace_search/account/create_source', - { - body: JSON.stringify({ - service_type: 'custom', - name: MOCK_NAME, - base_service_type: 'base_service_type', - }), - } - ); - await nextTick(); - expect(addSourceSuccessSpy).toHaveBeenCalledWith({ sourceConfigData }); - }); - - itShowsServerErrorAsFlashMessage(http.post, () => { - AddCustomSourceApiLogic.actions.addSource(MOCK_NAME); - }); - }); + }); + it('calls correct route for account', async () => { + const promise = Promise.resolve('result'); + AppLogic.values.isOrganization = false; + http.post.mockReturnValue(promise); + addCustomSource({ name: 'name' }); + await nextTick(); + expect(http.post).toHaveBeenCalledWith('/internal/workplace_search/account/create_source', { + body: JSON.stringify({ service_type: 'custom', name: 'name' }), }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_api_logic.ts index 4c4655a3882e0..a3e1afe4cdd11 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_api_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_api_logic.ts @@ -5,76 +5,35 @@ * 2.0. */ -import { kea, MakeLogicType } from 'kea'; - -import { ApiStatus, HttpError } from '../../../../../../../../common/types/api'; -import { flashAPIErrors, clearFlashMessages } from '../../../../../../shared/flash_messages'; +import { createApiLogic } from '../../../../../../shared/api_logic/create_api_logic'; import { HttpLogic } from '../../../../../../shared/http'; import { AppLogic } from '../../../../../app_logic'; import { CustomSource } from '../../../../../types'; -export interface AddCustomSourceApiActions { - addSource(name: string, baseServiceType?: string): { name: string; baseServiceType: string }; - addSourceError(code: number, error: string): HttpError; - addSourceSuccess(source: CustomSource): { source: CustomSource }; -} - -export interface AddCustomSourceApiValues { - sourceApi: ApiStatus; -} - -export const AddCustomSourceApiLogic = kea< - MakeLogicType ->({ - path: ['enterprise_search', 'workplace_search', 'add_custom_source_api_logic'], - actions: { - addSource: (name, baseServiceType) => ({ name, baseServiceType }), - addSourceError: (code, error) => ({ code, error }), - addSourceSuccess: (customSource) => ({ source: customSource }), - }, - reducers: () => ({ - sourceApi: [ - { - status: 'IDLE', - }, - { - addSource: () => ({ - status: 'PENDING', - }), - addSourceError: (_, error) => ({ - status: 'ERROR', - error, - }), - addSourceSuccess: (_, { source }) => ({ - status: 'SUCCESS', - data: source, - }), - }, - ], - }), - listeners: ({ actions }) => ({ - addSource: async ({ name, baseServiceType }) => { - clearFlashMessages(); - const { isOrganization } = AppLogic.values; - const route = isOrganization - ? '/internal/workplace_search/org/create_source' - : '/internal/workplace_search/account/create_source'; +export const addCustomSource = async ({ + name, + baseServiceType, +}: { + name: string; + baseServiceType?: string; +}) => { + const { isOrganization } = AppLogic.values; + const route = isOrganization + ? '/internal/workplace_search/org/create_source' + : '/internal/workplace_search/account/create_source'; - const params = { - service_type: 'custom', - name, - base_service_type: baseServiceType, - }; + const params = { + service_type: 'custom', + name, + base_service_type: baseServiceType, + }; + const source = await HttpLogic.values.http.post(route, { + body: JSON.stringify(params), + }); + return { source }; +}; - try { - const response = await HttpLogic.values.http.post(route, { - body: JSON.stringify(params), - }); - actions.addSourceSuccess(response); - } catch (e) { - flashAPIErrors(e); - actions.addSourceError(e?.body?.statusCode, e?.body?.message); - } - }, - }), -}); +export const AddCustomSourceApiLogic = createApiLogic( + ['workplace_search', 'add_custom_source_api_logic'], + addCustomSource +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_logic.test.ts index 59825c0bd386c..d62530454f7f7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_logic.test.ts @@ -7,12 +7,17 @@ import { LogicMounter } from '../../../../../../__mocks__/kea_logic'; +import { mockFlashMessageHelpers } from '../../../../../../__mocks__/kea_logic'; + +import { Status } from '../../../../../../../../common/types/api'; + jest.mock('../../../../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); import { CustomSource } from '../../../../../types'; +import { AddCustomSourceApiLogic } from './add_custom_source_api_logic'; import { AddCustomSourceLogic, AddCustomSourceSteps } from './add_custom_source_logic'; const DEFAULT_VALUES = { @@ -20,15 +25,14 @@ const DEFAULT_VALUES = { buttonLoading: false, customSourceNameValue: '', newCustomSource: {} as CustomSource, - sourceApi: { - status: 'IDLE', - }, + status: Status.IDLE, }; const MOCK_NAME = 'name'; describe('AddCustomSourceLogic', () => { const { mount } = new LogicMounter(AddCustomSourceLogic); + const { clearFlashMessages, flashAPIErrors } = mockFlashMessageHelpers; beforeEach(() => { jest.clearAllMocks(); @@ -40,27 +44,48 @@ describe('AddCustomSourceLogic', () => { }); describe('actions', () => { - describe('addSourceSuccess', () => { + describe('apiSuccess', () => { it('sets a new source', () => { - const customSource: CustomSource = { + AddCustomSourceLogic.actions.makeRequest({ name: 'name' }); + const source: CustomSource = { accessToken: 'a', name: 'b', id: '1', }; - AddCustomSourceLogic.actions.addSourceSuccess(customSource); + AddCustomSourceLogic.actions.apiSuccess({ source }); expect(AddCustomSourceLogic.values).toEqual({ ...DEFAULT_VALUES, customSourceNameValue: '', - newCustomSource: customSource, - sourceApi: { - status: 'SUCCESS', - data: customSource, - }, + newCustomSource: source, + status: Status.SUCCESS, currentStep: AddCustomSourceSteps.SaveCustomStep, }); }); }); + describe('makeRequest', () => { + it('sets button to loading', () => { + AddCustomSourceLogic.actions.makeRequest({ name: 'name' }); + + expect(AddCustomSourceLogic.values).toEqual({ + ...DEFAULT_VALUES, + buttonLoading: true, + status: Status.LOADING, + }); + }); + }); + describe('apiError', () => { + it('sets button to not loading', () => { + AddCustomSourceLogic.actions.makeRequest({ name: 'name' }); + AddCustomSourceLogic.actions.apiError('error' as any); + + expect(AddCustomSourceLogic.values).toEqual({ + ...DEFAULT_VALUES, + buttonLoading: false, + status: Status.ERROR, + }); + }); + }); describe('setCustomSourceNameValue', () => { it('saves the name', () => { AddCustomSourceLogic.actions.setCustomSourceNameValue('name'); @@ -98,33 +123,48 @@ describe('AddCustomSourceLogic', () => { customSourceNameValue: MOCK_NAME, }); }); - - describe('organization context', () => { - describe('createContentSource', () => { - it('calls addSource on AddCustomSourceApi logic', async () => { - const addSourceSpy = jest.spyOn(AddCustomSourceLogic.actions, 'addSource'); - - AddCustomSourceLogic.actions.createContentSource(); - expect(addSourceSpy).toHaveBeenCalledWith(MOCK_NAME, undefined); + describe('createContentSource', () => { + it('calls addSource on AddCustomSourceApi logic', async () => { + const addSourceSpy = jest.spyOn(AddCustomSourceLogic.actions, 'makeRequest'); + + AddCustomSourceLogic.actions.createContentSource(); + expect(addSourceSpy).toHaveBeenCalledWith({ + name: MOCK_NAME, + baseServiceType: undefined, }); + }); - it('submits a base service type for pre-configured sources', () => { - mount( - { - customSourceNameValue: MOCK_NAME, - }, - { - baseServiceType: 'share_point_server', - } - ); + it('submits a base service type for pre-configured sources', () => { + mount( + { + customSourceNameValue: MOCK_NAME, + }, + { + baseServiceType: 'share_point_server', + } + ); - const addSourceSpy = jest.spyOn(AddCustomSourceLogic.actions, 'addSource'); + const addSourceSpy = jest.spyOn(AddCustomSourceLogic.actions, 'makeRequest'); - AddCustomSourceLogic.actions.createContentSource(); + AddCustomSourceLogic.actions.createContentSource(); - expect(addSourceSpy).toHaveBeenCalledWith(MOCK_NAME, 'share_point_server'); + expect(addSourceSpy).toHaveBeenCalledWith({ + name: MOCK_NAME, + baseServiceType: 'share_point_server', }); }); }); + describe('makeRequest', () => { + it('should call clearFlashMessages', () => { + AddCustomSourceApiLogic.actions.makeRequest({ name: 'name' }); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); + describe('apiError', () => { + it('should call flashAPIError', () => { + AddCustomSourceApiLogic.actions.apiError('error' as any); + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_logic.ts index 76041744b3c37..0f81e78d98868 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_custom_source/add_custom_source_logic.ts @@ -7,13 +7,11 @@ import { kea, MakeLogicType } from 'kea'; +import { HttpError, Status } from '../../../../../../../../common/types/api'; +import { clearFlashMessages, flashAPIErrors } from '../../../../../../shared/flash_messages'; import { CustomSource } from '../../../../../types'; -import { - AddCustomSourceApiActions, - AddCustomSourceApiLogic, - AddCustomSourceApiValues, -} from './add_custom_source_api_logic'; +import { AddCustomSourceApiLogic } from './add_custom_source_api_logic'; export interface AddCustomSourceProps { baseServiceType?: string; @@ -26,8 +24,9 @@ export enum AddCustomSourceSteps { } export interface AddCustomSourceActions { - addSource: AddCustomSourceApiActions['addSource']; - addSourceSuccess: AddCustomSourceApiActions['addSourceSuccess']; + makeRequest: typeof AddCustomSourceApiLogic.actions.makeRequest; + apiSuccess({ source }: { source: CustomSource }): { source: CustomSource }; + apiError(error: HttpError): HttpError; createContentSource(): void; setCustomSourceNameValue(customSourceNameValue: string): string; setNewCustomSource(data: CustomSource): CustomSource; @@ -38,7 +37,7 @@ interface AddCustomSourceValues { currentStep: AddCustomSourceSteps; customSourceNameValue: string; newCustomSource: CustomSource; - sourceApi: AddCustomSourceApiValues['sourceApi']; + status: Status; } /** @@ -50,8 +49,8 @@ export const AddCustomSourceLogic = kea< MakeLogicType >({ connect: { - actions: [AddCustomSourceApiLogic, ['addSource', 'addSourceSuccess', 'addSourceError']], - values: [AddCustomSourceApiLogic, ['sourceApi']], + actions: [AddCustomSourceApiLogic, ['makeRequest', 'apiError', 'apiSuccess']], + values: [AddCustomSourceApiLogic, ['status']], }, path: ['enterprise_search', 'workplace_search', 'add_custom_source_logic'], actions: { @@ -64,8 +63,8 @@ export const AddCustomSourceLogic = kea< false, { createContentSource: () => true, - addSourceSuccess: () => false, - addSourceError: () => false, + apiSuccess: () => false, + apiError: () => false, }, ], currentStep: [ @@ -91,16 +90,15 @@ export const AddCustomSourceLogic = kea< createContentSource: () => { const { customSourceNameValue } = values; const { baseServiceType } = props; - actions.addSource(customSourceNameValue, baseServiceType); + actions.makeRequest({ name: customSourceNameValue, baseServiceType }); }, - addSourceSuccess: ({ source }) => { + makeRequest: () => clearFlashMessages(), + apiError: (error) => flashAPIErrors(error), + apiSuccess: ({ source }) => { actions.setNewCustomSource(source); }, }), selectors: { - buttonLoading: [ - (selectors) => [selectors.sourceApi], - (apiStatus) => apiStatus?.status === 'PENDING', - ], + buttonLoading: [(selectors) => [selectors.status], (status) => status === Status.LOADING], }, }); From ad10bf1334b801b50513d9d5da151ef991a86fcd Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 23 Jun 2022 12:35:44 +0100 Subject: [PATCH 22/54] [ML] Fix put job endpoint when payload contains datafeed (#134986) * [ML] Fix put job endpoint when payload contains datafeed * adding tests --- .../ml/server/routes/anomaly_detectors.ts | 14 ++- .../schemas/anomaly_detectors_schema.ts | 4 +- .../anomaly_detectors/create_with_datafeed.ts | 95 +++++++++++++++++++ .../apis/ml/anomaly_detectors/index.ts | 1 + 4 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 x-pack/test/api_integration/apis/ml/anomaly_detectors/create_with_datafeed.ts diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index 7989334c3852b..1534bcc2d7962 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -25,6 +25,7 @@ import { forceQuerySchema, jobResetQuerySchema, } from './schemas/anomaly_detectors_schema'; +import { getAuthorizationHeader } from '../lib/request_authorization'; /** * Routes for the anomaly detectors @@ -180,11 +181,14 @@ export function jobRoutes({ router, routeGuard }: RouteInitialization) { routeGuard.fullLicenseAPIGuard(async ({ mlClient, request, response }) => { try { const { jobId } = request.params; - const body = await mlClient.putJob({ - job_id: jobId, - // @ts-expect-error job type custom_rules is incorrect - body: request.body, - }); + const body = await mlClient.putJob( + { + job_id: jobId, + // @ts-expect-error job type custom_rules is incorrect + body: request.body, + }, + getAuthorizationHeader(request) + ); return response.ok({ body, diff --git a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts index 2b93a3a84457d..0fef223e1de55 100644 --- a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts @@ -6,6 +6,7 @@ */ import { schema } from '@kbn/config-schema'; +import { datafeedConfigSchema } from './datafeeds_schema'; const customRulesSchema = schema.maybe( schema.arrayOf( @@ -121,7 +122,7 @@ export const anomalyDetectionJobSchema = { description: schema.maybe(schema.string()), established_model_memory: schema.maybe(schema.number()), finished_time: schema.maybe(schema.number()), - job_id: schema.string(), + job_id: schema.maybe(schema.string()), job_type: schema.maybe(schema.string()), job_version: schema.maybe(schema.string()), groups: schema.maybe(schema.arrayOf(schema.maybe(schema.string()))), @@ -136,6 +137,7 @@ export const anomalyDetectionJobSchema = { results_index_name: schema.maybe(schema.string()), results_retention_days: schema.maybe(schema.number()), state: schema.maybe(schema.string()), + datafeed_config: schema.maybe(datafeedConfigSchema), }; export const jobIdSchema = schema.object({ diff --git a/x-pack/test/api_integration/apis/ml/anomaly_detectors/create_with_datafeed.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create_with_datafeed.ts new file mode 100644 index 0000000000000..b0f2976ff3343 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create_with_datafeed.ts @@ -0,0 +1,95 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const jobId = `fq_single_${Date.now()}`; + + const testDataList = [ + { + testTitle: 'ML Poweruser creates a single metric job with datafeed', + user: USER.ML_POWERUSER, + jobId: `${jobId}_1`, + requestBody: { + job_id: `${jobId}_1`, + description: + 'Single metric job based on the farequote dataset with 30m bucketspan and mean(responsetime)', + groups: ['automated', 'farequote', 'single-metric'], + analysis_config: { + bucket_span: '30m', + detectors: [{ function: 'mean', field_name: 'responsetime' }], + influencers: [], + summary_count_field_name: 'doc_count', + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '11MB' }, + model_plot_config: { enabled: true }, + datafeed_config: { + datafeed_id: `datafeed-${jobId}_1`, + indices: ['farequote-*'], + query: { + match_all: {}, + }, + }, + }, + expected: { + responseCode: 200, + responseBody: { + // skipping parts of the job config we're not going to check + // we're only interesting in the datafeed_config for this test + datafeed_config: { + job_id: `${jobId}_1`, + datafeed_id: `datafeed-${jobId}_1`, + indices: ['farequote-*'], + query: { + match_all: {}, + }, + }, + }, + }, + }, + ]; + + describe('PUT anomaly_detectors which contain a datafeed config', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const { body, status } = await supertest + .put(`/api/ml/anomaly_detectors/${testData.jobId}`) + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) + .set(COMMON_REQUEST_HEADERS) + .send(testData.requestBody); + ml.api.assertResponseStatusCode(testData.expected.responseCode, status, body); + + // Validate the important parts of the response. + const expectedResponse = testData.expected.responseBody; + expect(body.datafeed_config.datafeed_id).to.eql( + expectedResponse.datafeed_config.datafeed_id + ); + expect(body.datafeed_config.job_id).to.eql(expectedResponse.datafeed_config.job_id); + expect(body.datafeed_config.indices).to.eql(expectedResponse.datafeed_config.indices); + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/anomaly_detectors/index.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/index.ts index 3f854f6649fda..89a12cb7f9a76 100644 --- a/x-pack/test/api_integration/apis/ml/anomaly_detectors/index.ts +++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/index.ts @@ -18,5 +18,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./delete_with_spaces')); loadTestFile(require.resolve('./create_with_spaces')); loadTestFile(require.resolve('./forecast_with_spaces')); + loadTestFile(require.resolve('./create_with_datafeed')); }); } From be71c903b9bc5b90bf211f974184fd9a8200237f Mon Sep 17 00:00:00 2001 From: Byron Hulcher Date: Thu, 23 Jun 2022 07:48:41 -0400 Subject: [PATCH 23/54] [Workplace Search] Hide integration tiles for service not launching with 8.3.0 (#134931) --- .../apis/custom_integration/integrations.ts | 2 +- .../shared/assets/source_icons/index.ts | 6 --- .../shared/assets/source_icons/outlook.svg | 1 - .../shared/assets/source_icons/teams.svg | 1 - .../shared/assets/source_icons/zoom.svg | 1 - .../available_sources_list.test.tsx | 2 +- .../configured_sources_list.test.tsx | 4 +- .../add_source/connect_instance.test.tsx | 4 +- .../views/content_sources/source_data.tsx | 53 ------------------- .../content_sources/sources_logic.test.ts | 2 +- .../public/assets/source_icons/outlook.svg | 1 - .../public/assets/source_icons/teams.svg | 1 - .../public/assets/source_icons/zoom.svg | 1 - .../enterprise_search/server/integrations.ts | 44 --------------- 14 files changed, 7 insertions(+), 116 deletions(-) delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/outlook.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/teams.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/zoom.svg delete mode 100644 x-pack/plugins/enterprise_search/public/assets/source_icons/outlook.svg delete mode 100644 x-pack/plugins/enterprise_search/public/assets/source_icons/teams.svg delete mode 100644 x-pack/plugins/enterprise_search/public/assets/source_icons/zoom.svg diff --git a/test/api_integration/apis/custom_integration/integrations.ts b/test/api_integration/apis/custom_integration/integrations.ts index c4fda918328f8..a6fe5c23c1b70 100644 --- a/test/api_integration/apis/custom_integration/integrations.ts +++ b/test/api_integration/apis/custom_integration/integrations.ts @@ -22,7 +22,7 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body).to.be.an('array'); - expect(resp.body.length).to.be(42); + expect(resp.body.length).to.be(39); // Test for sample data card expect(resp.body.findIndex((c: { id: string }) => c.id === 'sample_data_all')).to.be.above( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts index 0673617632a30..5a2bb892fb80c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts @@ -18,15 +18,12 @@ import jiraServer from './jira_server.svg'; import loadingSmall from './loading_small.svg'; import networkDrive from './network_drive.svg'; import oneDrive from './onedrive.svg'; -import outlook from './outlook.svg'; import salesforce from './salesforce.svg'; import serviceNow from './servicenow.svg'; import sharePoint from './sharepoint.svg'; import sharePointServer from './sharepoint_server.svg'; import slack from './slack.svg'; -import teams from './teams.svg'; import zendesk from './zendesk.svg'; -import zoom from './zoom.svg'; export const images = { box, @@ -48,14 +45,11 @@ export const images = { loadingSmall, networkDrive, oneDrive, - outlook, salesforce, salesforceSandbox: salesforce, serviceNow, sharePoint, sharePointServer, slack, - teams, zendesk, - zoom, } as { [key: string]: string }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/outlook.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/outlook.svg deleted file mode 100644 index 932a3756a522b..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/outlook.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/teams.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/teams.svg deleted file mode 100644 index bf3aef9277eb6..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/teams.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/zoom.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/zoom.svg deleted file mode 100644 index d7498817bc62d..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/zoom.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.test.tsx index e0264a20f8a54..9020612537c80 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.test.tsx @@ -26,7 +26,7 @@ describe('AvailableSourcesList', () => { const wrapper = shallow(); expect(wrapper.find(EuiTitle)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="AvailableSourceListItem"]')).toHaveLength(26); + expect(wrapper.find('[data-test-subj="AvailableSourceListItem"]')).toHaveLength(23); expect(wrapper.find('[data-test-subj="CustomAPISourceLink"]')).toHaveLength(1); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx index a9a766e775e49..ad3a43b4f8e94 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx @@ -26,9 +26,9 @@ describe('ConfiguredSourcesList', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="UnConnectedTooltip"]')).toHaveLength(22); + expect(wrapper.find('[data-test-subj="UnConnectedTooltip"]')).toHaveLength(19); expect(wrapper.find('[data-test-subj="AccountOnlyTooltip"]')).toHaveLength(2); - expect(wrapper.find('[data-test-subj="ConfiguredSourcesListItem"]')).toHaveLength(25); + expect(wrapper.find('[data-test-subj="ConfiguredSourcesListItem"]')).toHaveLength(22); }); it('shows connect button for an source with multiple connector options that routes to choice page', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx index 8134e4337b6cf..d654360b40fa2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx @@ -40,9 +40,9 @@ describe('ConnectInstance', () => { redirectFormCreated(); }); - const credentialsSourceData = staticSourceData[16]; // service_now + const credentialsSourceData = staticSourceData[15]; // service_now const oauthSourceData = staticSourceData[0]; // box - const subdomainSourceData = staticSourceData[22]; // zendesk + const subdomainSourceData = staticSourceData[20]; // zendesk const props = { ...credentialsSourceData, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx index 6188c37b20057..4b1bd88dd045b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx @@ -474,25 +474,6 @@ export const staticSourceData: SourceDataItem[] = [ }, accountContextOnly: false, }, - { - name: SOURCE_NAMES.OUTLOOK, - categories: [ - SOURCE_CATEGORIES.COMMUNICATION, - SOURCE_CATEGORIES.PRODUCTIVITY, - SOURCE_CATEGORIES.MICROSOFT, - ], - serviceType: 'custom', - baseServiceType: 'outlook', - configuration: { - isPublicKey: false, - hasOauthRedirect: false, - needsBaseUrl: false, - documentationUrl: docLinks.workplaceSearchOutlook, - applicationPortalUrl: '', - githubRepository: 'elastic/enterprise-search-outlook-connector', - }, - accountContextOnly: false, - }, { name: SOURCE_NAMES.SALESFORCE, serviceType: 'salesforce', @@ -721,25 +702,6 @@ export const staticSourceData: SourceDataItem[] = [ }, accountContextOnly: true, }, - { - name: SOURCE_NAMES.TEAMS, - categories: [ - SOURCE_CATEGORIES.COMMUNICATION, - SOURCE_CATEGORIES.PRODUCTIVITY, - SOURCE_CATEGORIES.MICROSOFT, - ], - serviceType: 'custom', - baseServiceType: 'teams', - configuration: { - isPublicKey: false, - hasOauthRedirect: false, - needsBaseUrl: false, - documentationUrl: docLinks.workplaceSearchTeams, - applicationPortalUrl: '', - githubRepository: 'elastic/enterprise-search-teams-connector', - }, - accountContextOnly: false, - }, { name: SOURCE_NAMES.ZENDESK, serviceType: 'zendesk', @@ -774,21 +736,6 @@ export const staticSourceData: SourceDataItem[] = [ }, accountContextOnly: false, }, - { - name: SOURCE_NAMES.ZOOM, - categories: [SOURCE_CATEGORIES.COMMUNICATION, SOURCE_CATEGORIES.PRODUCTIVITY], - serviceType: 'custom', - baseServiceType: 'zoom', - configuration: { - isPublicKey: false, - hasOauthRedirect: false, - needsBaseUrl: false, - documentationUrl: docLinks.workplaceSearchZoom, - applicationPortalUrl: '', - githubRepository: 'elastic/enterprise-search-zoom-connector', - }, - accountContextOnly: false, - }, staticGenericExternalSourceData, ]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts index 8eeb657f23800..75c9010ae9be5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts @@ -335,7 +335,7 @@ describe('SourcesLogic', () => { it('availableSources & configuredSources have correct length', () => { SourcesLogic.actions.onInitializeSources(serverResponse); - expect(SourcesLogic.values.availableSources).toHaveLength(17); + expect(SourcesLogic.values.availableSources).toHaveLength(14); expect(SourcesLogic.values.configuredSources).toHaveLength(5); }); it('externalConfigured is set to true if external is configured', () => { diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/outlook.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/outlook.svg deleted file mode 100644 index 932a3756a522b..0000000000000 --- a/x-pack/plugins/enterprise_search/public/assets/source_icons/outlook.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/teams.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/teams.svg deleted file mode 100644 index bf3aef9277eb6..0000000000000 --- a/x-pack/plugins/enterprise_search/public/assets/source_icons/teams.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/zoom.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/zoom.svg deleted file mode 100644 index d7498817bc62d..0000000000000 --- a/x-pack/plugins/enterprise_search/public/assets/source_icons/zoom.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/server/integrations.ts b/x-pack/plugins/enterprise_search/server/integrations.ts index ec3a13f526528..0b0be51f9399e 100644 --- a/x-pack/plugins/enterprise_search/server/integrations.ts +++ b/x-pack/plugins/enterprise_search/server/integrations.ts @@ -191,20 +191,6 @@ const workplaceSearchIntegrations: WorkplaceSearchIntegration[] = [ categories: ['file_storage'], uiInternalPath: '/app/enterprise_search/workplace_search/sources/add/one_drive', }, - { - id: 'outlook', - title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.outlookName', { - defaultMessage: 'Outlook', - }), - description: i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.integrations.outlookDescription', - { - defaultMessage: 'Search over your email and calendars with Enterprise Search.', - } - ), - categories: ['enterprise_search', 'microsoft_365', 'communications', 'productivity'], - uiInternalPath: '/app/enterprise_search/workplace_search/sources/add/outlook/custom', - }, { id: 'salesforce', title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.salesforceName', { @@ -295,21 +281,6 @@ const workplaceSearchIntegrations: WorkplaceSearchIntegration[] = [ ), categories: ['communications'], }, - { - id: 'teams', - title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.teamsName', { - defaultMessage: 'Teams', - }), - description: i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.integrations.teamsDescription', - { - defaultMessage: - 'Search over meeting recordings, chats and other communications with Enterprise Search.', - } - ), - categories: ['enterprise_search', 'microsoft_365', 'communications', 'productivity'], - uiInternalPath: '/app/enterprise_search/workplace_search/sources/add/teams/custom', - }, { id: 'zendesk', title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.zendeskName', { @@ -323,21 +294,6 @@ const workplaceSearchIntegrations: WorkplaceSearchIntegration[] = [ ), categories: ['communications'], }, - { - id: 'zoom', - title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.zoomName', { - defaultMessage: 'Zoom', - }), - description: i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.integrations.zoomDescription', - { - defaultMessage: - 'Search over meeting recordings, chats and other communications with Enterprise Search.', - } - ), - categories: ['enterprise_search', 'communications', 'productivity'], - uiInternalPath: '/app/enterprise_search/workplace_search/sources/add/zoom/custom', - }, ]; export const registerEnterpriseSearchIntegrations = ( From 501927443ada5719c2a61fd06a703be3505f4c13 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 23 Jun 2022 06:59:51 -0600 Subject: [PATCH 24/54] fix onDataLoadEnd and onDataLoadError event handler callbacks only called for source data requests (#134786) * fix onDataLoadEnd and onDataLoadError event handler callbacks only called for source data requests * dataLoadEnd Co-authored-by: Liza Katz --- .../public/actions/data_request_actions.ts | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/maps/public/actions/data_request_actions.ts b/x-pack/plugins/maps/public/actions/data_request_actions.ts index 0a1c5b8d7fae4..8c521fa40b0cd 100644 --- a/x-pack/plugins/maps/public/actions/data_request_actions.ts +++ b/x-pack/plugins/maps/public/actions/data_request_actions.ts @@ -287,27 +287,27 @@ function endDataLoad( throw new DataRequestAbortError(); } - if (dataId === SOURCE_DATA_REQUEST_ID) { - const features = data && 'features' in data ? (data as FeatureCollection).features : []; - const layer = getLayerById(layerId, getState()); - - const eventHandlers = getEventHandlers(getState()); - if (eventHandlers && eventHandlers.onDataLoadEnd) { - const resultMeta: ResultMeta = {}; - if (layer && layer.getType() === LAYER_TYPE.GEOJSON_VECTOR) { - const featuresWithoutCentroids = features.filter((feature) => { - return feature.properties ? !feature.properties[KBN_IS_CENTROID_FEATURE] : true; - }); - resultMeta.featuresCount = featuresWithoutCentroids.length; - } + const features = data && 'features' in data ? (data as FeatureCollection).features : []; + const layer = getLayerById(layerId, getState()); - eventHandlers.onDataLoadEnd({ - layerId, - dataId, - resultMeta, + const eventHandlers = getEventHandlers(getState()); + if (eventHandlers && eventHandlers.onDataLoadEnd) { + const resultMeta: ResultMeta = {}; + if (layer && layer.getType() === LAYER_TYPE.GEOJSON_VECTOR) { + const featuresWithoutCentroids = features.filter((feature) => { + return feature.properties ? !feature.properties[KBN_IS_CENTROID_FEATURE] : true; }); + resultMeta.featuresCount = featuresWithoutCentroids.length; } + eventHandlers.onDataLoadEnd({ + layerId, + dataId, + resultMeta, + }); + } + + if (dataId === SOURCE_DATA_REQUEST_ID) { if (layer) { dispatch(updateTooltipStateForLayer(layer, features)); } @@ -343,16 +343,16 @@ function onDataLoadError( ) => { dispatch(unregisterCancelCallback(requestToken)); - if (dataId === SOURCE_DATA_REQUEST_ID) { - const eventHandlers = getEventHandlers(getState()); - if (eventHandlers && eventHandlers.onDataLoadError) { - eventHandlers.onDataLoadError({ - layerId, - dataId, - errorMessage, - }); - } + const eventHandlers = getEventHandlers(getState()); + if (eventHandlers && eventHandlers.onDataLoadError) { + eventHandlers.onDataLoadError({ + layerId, + dataId, + errorMessage, + }); + } + if (dataId === SOURCE_DATA_REQUEST_ID) { const layer = getLayerById(layerId, getState()); if (layer) { dispatch(updateTooltipStateForLayer(layer)); From ec3e3b27c6b0bbe3237098fc32a454e476bf2f83 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 23 Jun 2022 15:49:53 +0200 Subject: [PATCH 25/54] [APM] Backend operation distribution chart (#134561) * [APM] Backend operation distribution chart Closes #133483. * Update labels * Take sampleRangeFrom/To into account for span operations table * Remove console.log statements * Review feedback * Add event.outcome and truncated trace id to table * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Update API tests Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- src/core/types/elasticsearch/search.ts | 9 + .../failed_transactions_correlations/types.ts | 2 +- .../common/correlations/field_stats_types.ts | 3 - .../latency_correlations/types.ts | 2 +- .../plugins/apm/common/correlations/types.ts | 21 +- .../backend_detail_operations_list/index.tsx | 4 +- .../backend_detail_dependencies_table.tsx | 15 +- .../backend_operation_detail_trace_list.tsx | 156 +++++-- .../backend_operation_distribution_chart.tsx | 110 +++++ .../backend_operation_detail_view/index.tsx | 12 +- .../context_popover/top_values.tsx | 19 +- .../failed_transactions_correlations.tsx | 8 +- ...get_transaction_distribution_chart_data.ts | 5 +- .../app/correlations/latency_correlations.tsx | 8 +- ..._failed_transactions_correlations.test.tsx | 103 ++--- .../use_failed_transactions_correlations.ts | 56 +-- .../use_latency_correlations.test.tsx | 92 +++-- .../correlations/use_latency_correlations.ts | 10 +- .../distribution/index.test.tsx | 14 +- .../distribution/index.tsx | 157 +------ ...use_transaction_distribution_chart_data.ts | 46 ++- .../transaction_details_tabs.tsx | 36 +- .../public/components/routing/home/index.tsx | 14 +- .../index.test.tsx | 0 .../index.tsx | 52 ++- .../index.test.ts | 20 + .../index.tsx | 180 ++++++++ .../hooks/use_sample_chart_selection.ts | 46 +++ .../create_apm_event_client/index.ts | 190 +++++---- .../get_backend_latency_distribution.ts | 84 ++++ .../routes/backends/get_top_backend_spans.ts | 24 ++ .../apm/server/routes/backends/route.ts | 65 ++- .../queries/fetch_duration_correlation.ts | 104 +++++ ...tch_duration_correlation_with_histogram.ts | 101 +++++ ....ts => fetch_duration_field_candidates.ts} | 81 ++-- .../queries/fetch_duration_fractions.ts | 79 ++++ .../fetch_duration_histogram_range_steps.ts | 89 ++++ .../queries/fetch_duration_percentiles.ts | 78 ++++ .../queries/fetch_duration_ranges.ts | 88 ++++ ...etch_failed_events_correlation_p_values.ts | 138 +++++++ .../queries/fetch_field_value_pairs.ts | 78 ++++ .../{query_p_values.ts => fetch_p_values.ts} | 69 ++-- ...s.ts => fetch_significant_correlations.ts} | 126 ++++-- .../field_stats/fetch_boolean_field_stats.ts | 76 ++++ .../fetch_field_value_field_stats.ts | 79 ++++ .../queries/field_stats/fetch_fields_stats.ts | 139 +++++++ .../field_stats/fetch_keyword_field_stats.ts | 71 ++++ .../field_stats/fetch_numeric_field_stats.ts | 97 +++++ .../field_stats/get_boolean_field_stats.ts | 87 ---- .../field_stats/get_field_stats.test.ts | 140 ------- .../field_stats/get_field_value_stats.ts | 76 ---- .../queries/field_stats/get_fields_stats.ts | 103 ----- .../field_stats/get_keyword_field_stats.ts | 83 ---- .../field_stats/get_numeric_field_stats.ts | 107 ----- .../queries/get_common_correlations_query.ts | 30 ++ .../correlations/queries/get_filters.ts | 48 --- .../queries/get_query_with_params.test.ts | 132 ------ .../queries/get_query_with_params.ts | 52 --- .../queries/get_request_base.test.ts | 36 -- .../correlations/queries/get_request_base.ts | 18 - .../routes/correlations/queries/index.ts | 19 - .../queries/query_correlation.test.ts | 111 ----- .../correlations/queries/query_correlation.ts | 138 ------- .../query_correlation_with_histogram.test.ts | 116 ------ .../query_correlation_with_histogram.ts | 70 ---- .../queries/query_failure_correlation.ts | 127 ------ .../queries/query_field_candidates.test.ts | 161 -------- .../queries/query_field_value_pairs.test.ts | 78 ---- .../queries/query_field_value_pairs.ts | 91 ----- .../queries/query_fractions.test.ts | 72 ---- .../correlations/queries/query_fractions.ts | 79 ---- .../queries/query_histogram.test.ts | 107 ----- .../correlations/queries/query_histogram.ts | 62 --- .../query_histogram_range_steps.test.ts | 108 ----- .../queries/query_histogram_range_steps.ts | 78 ---- .../queries/query_percentiles.test.ts | 112 ----- .../correlations/queries/query_percentiles.ts | 82 ---- .../correlations/queries/query_ranges.test.ts | 141 ------- .../correlations/queries/query_ranges.ts | 87 ---- .../apm/server/routes/correlations/route.ts | 385 +++++++++++------- .../get_overall_latency_distribution.ts | 148 +++---- .../get_percentile_threshold_value.ts | 73 ++-- .../routes/latency_distribution/route.ts | 34 +- .../routes/latency_distribution/types.ts | 12 +- .../translations/translations/fr-FR.json | 15 +- .../translations/translations/ja-JP.json | 13 +- .../translations/translations/zh-CN.json | 15 +- .../correlations/failed_transactions.spec.ts | 18 +- .../correlations/field_candidates.spec.ts | 2 +- .../correlations/field_value_pairs.spec.ts | 2 +- .../tests/correlations/latency.spec.ts | 18 +- .../tests/correlations/p_values.spec.ts | 2 +- .../significant_correlations.spec.ts | 2 +- .../tests/dependencies/top_spans.spec.ts | 25 ++ .../latency_overall_distribution.spec.ts | 2 +- 95 files changed, 2869 insertions(+), 3704 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/backend_operation_detail_view/backend_operation_distribution_chart.tsx rename x-pack/plugins/apm/public/components/shared/charts/{transaction_distribution_chart => duration_distribution_chart}/index.test.tsx (100%) rename x-pack/plugins/apm/public/components/shared/charts/{transaction_distribution_chart => duration_distribution_chart}/index.tsx (84%) create mode 100644 x-pack/plugins/apm/public/components/shared/charts/duration_distribution_chart_with_scrubber/index.test.ts create mode 100644 x-pack/plugins/apm/public/components/shared/charts/duration_distribution_chart_with_scrubber/index.tsx create mode 100644 x-pack/plugins/apm/public/hooks/use_sample_chart_selection.ts create mode 100644 x-pack/plugins/apm/server/routes/backends/get_backend_latency_distribution.ts create mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_correlation.ts create mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_correlation_with_histogram.ts rename x-pack/plugins/apm/server/routes/correlations/queries/{query_field_candidates.ts => fetch_duration_field_candidates.ts} (62%) create mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_fractions.ts create mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_histogram_range_steps.ts create mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_percentiles.ts create mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_ranges.ts create mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/fetch_failed_events_correlation_p_values.ts create mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/fetch_field_value_pairs.ts rename x-pack/plugins/apm/server/routes/correlations/queries/{query_p_values.ts => fetch_p_values.ts} (58%) rename x-pack/plugins/apm/server/routes/correlations/queries/{query_significant_correlations.ts => fetch_significant_correlations.ts} (53%) create mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/field_stats/fetch_boolean_field_stats.ts create mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/field_stats/fetch_field_value_field_stats.ts create mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/field_stats/fetch_fields_stats.ts create mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/field_stats/fetch_keyword_field_stats.ts create mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/field_stats/fetch_numeric_field_stats.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_boolean_field_stats.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_stats.test.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_value_stats.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_fields_stats.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_keyword_field_stats.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_numeric_field_stats.ts create mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/get_common_correlations_query.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/get_filters.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/get_query_with_params.test.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/get_query_with_params.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/get_request_base.test.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/get_request_base.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/index.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/query_correlation.test.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/query_correlation.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/query_correlation_with_histogram.test.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/query_correlation_with_histogram.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/query_failure_correlation.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/query_field_candidates.test.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/query_field_value_pairs.test.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/query_field_value_pairs.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/query_fractions.test.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/query_fractions.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/query_histogram.test.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/query_histogram.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/query_histogram_range_steps.test.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/query_histogram_range_steps.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/query_percentiles.test.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/query_percentiles.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/query_ranges.test.ts delete mode 100644 x-pack/plugins/apm/server/routes/correlations/queries/query_ranges.ts diff --git a/src/core/types/elasticsearch/search.ts b/src/core/types/elasticsearch/search.ts index 96d1bec3c5f1e..036f85177d305 100644 --- a/src/core/types/elasticsearch/search.ts +++ b/src/core/types/elasticsearch/search.ts @@ -147,6 +147,14 @@ export type AggregateOf< q2: number | null; q3: number | null; }; + bucket_correlation: { + value: number | null; + }; + bucket_count_ks_test: { + less: number; + greater: number; + two_sided: number; + }; bucket_script: { value: unknown; }; @@ -536,6 +544,7 @@ export type AggregateOf< { doc_count: number; key: string | number; + key_as_string?: string; } & SubAggregateOf >; }; diff --git a/x-pack/plugins/apm/common/correlations/failed_transactions_correlations/types.ts b/x-pack/plugins/apm/common/correlations/failed_transactions_correlations/types.ts index e63d3d6faa92e..ae9a73f21ed02 100644 --- a/x-pack/plugins/apm/common/correlations/failed_transactions_correlations/types.ts +++ b/x-pack/plugins/apm/common/correlations/failed_transactions_correlations/types.ts @@ -27,7 +27,7 @@ export type FailedTransactionsCorrelationsImpactThreshold = export interface FailedTransactionsCorrelationsResponse { ccsWarning: boolean; failedTransactionsCorrelations?: FailedTransactionsCorrelation[]; - percentileThresholdValue?: number; + percentileThresholdValue?: number | null; overallHistogram?: HistogramItem[]; errorHistogram?: HistogramItem[]; fieldStats?: FieldStats[]; diff --git a/x-pack/plugins/apm/common/correlations/field_stats_types.ts b/x-pack/plugins/apm/common/correlations/field_stats_types.ts index 41f7e3c3c6649..b350a0ff9e639 100644 --- a/x-pack/plugins/apm/common/correlations/field_stats_types.ts +++ b/x-pack/plugins/apm/common/correlations/field_stats_types.ts @@ -6,9 +6,6 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { CorrelationsParams } from './types'; - -export type FieldStatsCommonRequestParams = CorrelationsParams; export interface Field { fieldName: string; diff --git a/x-pack/plugins/apm/common/correlations/latency_correlations/types.ts b/x-pack/plugins/apm/common/correlations/latency_correlations/types.ts index cf20490774e18..8b6586e5b6fa2 100644 --- a/x-pack/plugins/apm/common/correlations/latency_correlations/types.ts +++ b/x-pack/plugins/apm/common/correlations/latency_correlations/types.ts @@ -18,7 +18,7 @@ export interface LatencyCorrelation extends FieldValuePair { export interface LatencyCorrelationsResponse { ccsWarning: boolean; overallHistogram?: HistogramItem[]; - percentileThresholdValue?: number; + percentileThresholdValue?: number | null; latencyCorrelations?: LatencyCorrelation[]; fieldStats?: FieldStats[]; } diff --git a/x-pack/plugins/apm/common/correlations/types.ts b/x-pack/plugins/apm/common/correlations/types.ts index 6884d8c627fd0..a03ce73b76db6 100644 --- a/x-pack/plugins/apm/common/correlations/types.ts +++ b/x-pack/plugins/apm/common/correlations/types.ts @@ -5,6 +5,9 @@ * 2.0. */ +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { Environment } from '../environment_rt'; + export interface FieldValuePair { fieldName: string; // For dynamic fieldValues we only identify fields as `string`, @@ -27,20 +30,10 @@ export interface ResponseHit { _source: ResponseHitSource; } -export interface CorrelationsClientParams { - environment: string; - kuery: string; - serviceName?: string; - transactionName?: string; - transactionType?: string; +export interface CommonCorrelationsQueryParams { start: number; end: number; + kuery: string; + environment: Environment; + query: QueryDslQueryContainer; } - -export interface CorrelationsServerParams { - index: string; - includeFrozen?: boolean; -} - -export type CorrelationsParams = CorrelationsClientParams & - CorrelationsServerParams; diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_operations/backend_detail_operations_list/index.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_operations/backend_detail_operations_list/index.tsx index 080852f017cd8..5b2c624af48b4 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_operations/backend_detail_operations_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_operations/backend_detail_operations_list/index.tsx @@ -86,7 +86,9 @@ export function BackendDetailOperationsList() { const comparisonStatsFetch = useFetcher( (callApmApi) => { if (!comparisonEnabled) { - return; + return Promise.resolve({ + operations: [], + }); } return callApmApi('GET /internal/apm/backends/operations', { params: { diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx index 4bdec499b919f..87733a69930d6 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx @@ -14,6 +14,8 @@ import { useFetcher } from '../../../hooks/use_fetcher'; import { DependenciesTable } from '../../shared/dependencies_table'; import { ServiceLink } from '../../shared/service_link'; import { useTimeRange } from '../../../hooks/use_time_range'; +import { getComparisonEnabled } from '../../shared/time_comparison/get_comparison_enabled'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; export function BackendDetailDependenciesTable() { const { @@ -23,19 +25,22 @@ export function BackendDetailDependenciesTable() { rangeTo, kuery, environment, - comparisonEnabled, + comparisonEnabled: urlComparisonEnabled, offset, }, } = useApmParams('/backends/overview'); + const { core } = useApmPluginContext(); + + const comparisonEnabled = getComparisonEnabled({ + core, + urlComparisonEnabled, + }); + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); const { data, status } = useFetcher( (callApmApi) => { - if (!start || !end) { - return; - } - return callApmApi('GET /internal/apm/backends/upstream_services', { params: { query: { diff --git a/x-pack/plugins/apm/public/components/app/backend_operation_detail_view/backend_operation_detail_trace_list.tsx b/x-pack/plugins/apm/public/components/app/backend_operation_detail_view/backend_operation_detail_trace_list.tsx index d43e4c0c7a72e..d0069fcc7447f 100644 --- a/x-pack/plugins/apm/public/components/app/backend_operation_detail_view/backend_operation_detail_trace_list.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_operation_detail_view/backend_operation_detail_trace_list.tsx @@ -5,6 +5,7 @@ * 2.0. */ import { + EuiBadge, EuiFlexGroup, EuiFlexItem, EuiLink, @@ -15,10 +16,12 @@ import { import { i18n } from '@kbn/i18n'; import React from 'react'; import { ValuesType } from 'utility-types'; +import { EventOutcome } from '../../../../common/event_outcome'; import { asMillisecondDuration } from '../../../../common/utils/formatters'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useApmRouter } from '../../../hooks/use_apm_router'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { useTheme } from '../../../hooks/use_theme'; import { useTimeRange } from '../../../hooks/use_time_range'; import { APIReturnType } from '../../../services/rest/create_call_apm_api'; import { ITableColumn, ManagedTable } from '../../shared/managed_table'; @@ -32,6 +35,8 @@ type BackendSpan = ValuesType< export function BackendOperationDetailTraceList() { const router = useApmRouter(); + const theme = useTheme(); + const { query: { backendName, @@ -44,12 +49,109 @@ export function BackendOperationDetailTraceList() { refreshInterval, refreshPaused, kuery, + sampleRangeFrom, + sampleRangeTo, }, } = useApmParams('/backends/operation'); + function getTraceLink({ + transactionName, + transactionType, + traceId, + transactionId, + serviceName, + }: { + serviceName: string; + transactionName?: string; + transactionType?: string; + traceId: string; + transactionId?: string; + }) { + const href = transactionName + ? router.link('/services/{serviceName}/transactions/view', { + path: { serviceName }, + query: { + comparisonEnabled, + environment, + kuery, + rangeFrom, + rangeTo, + serviceGroup: '', + transactionName, + refreshInterval, + refreshPaused, + offset, + traceId, + transactionId, + transactionType, + }, + }) + : router.link('/link-to/trace/{traceId}', { + path: { + traceId, + }, + query: { + rangeFrom, + rangeTo, + }, + }); + + return href; + } + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); const columns: Array> = [ + { + name: i18n.translate( + 'xpack.apm.backendOperationDetailTraceListOutcomeColumn', + { defaultMessage: 'Outcome' } + ), + field: 'outcome', + render: (_, { outcome }) => { + let color: string; + if (outcome === EventOutcome.success) { + color = theme.eui.euiColorSuccess; + } else if (outcome === EventOutcome.failure) { + color = theme.eui.euiColorDanger; + } else { + color = theme.eui.euiColorMediumShade; + } + + return {outcome}; + }, + }, + { + name: i18n.translate( + 'xpack.apm.backendOperationDetailTraceListTraceIdColumn', + { defaultMessage: 'Trace' } + ), + field: 'traceId', + render: ( + _, + { + serviceName, + traceId, + transactionId, + transactionName, + transactionType, + } + ) => { + const href = getTraceLink({ + serviceName, + traceId, + transactionId, + transactionType, + transactionName, + }); + + return ( + + {traceId.substr(0, 6)} + + ); + }, + }, { name: i18n.translate( 'xpack.apm.backendOperationDetailTraceListServiceNameColumn', @@ -77,7 +179,6 @@ export function BackendOperationDetailTraceList() { /> ); }, - width: '30%', sortable: true, }, { @@ -96,38 +197,16 @@ export function BackendOperationDetailTraceList() { transactionType, } ) => { - const href = transactionName - ? router.link('/services/{serviceName}/transactions/view', { - path: { serviceName }, - query: { - comparisonEnabled, - environment, - kuery, - rangeFrom, - rangeTo, - serviceGroup: '', - transactionName, - refreshInterval, - refreshPaused, - offset, - traceId, - transactionId, - transactionType, - }, - }) - : router.link('/link-to/trace/{traceId}', { - path: { - traceId, - }, - query: { - rangeFrom, - rangeTo, - }, - }); + const href = getTraceLink({ + serviceName, + transactionName, + traceId, + transactionId, + transactionType, + }); return {transactionName || traceId}; }, - width: '50%', sortable: true, }, { @@ -167,11 +246,22 @@ export function BackendOperationDetailTraceList() { end, environment, kuery, + sampleRangeFrom, + sampleRangeTo, }, }, }); }, - [backendName, spanName, start, end, environment, kuery] + [ + backendName, + spanName, + start, + end, + environment, + kuery, + sampleRangeFrom, + sampleRangeTo, + ] ); return ( @@ -189,8 +279,8 @@ export function BackendOperationDetailTraceList() { = 0 && sampleRangeTo > 0 + ? [sampleRangeFrom, sampleRangeTo] + : undefined; + + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); + + const { status, data } = useFetcher( + (callApmApi) => { + return callApmApi('GET /internal/apm/backends/charts/distribution', { + params: { + query: { + percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + backendName, + spanName, + environment, + kuery, + start, + end, + }, + }, + }); + }, + [backendName, spanName, environment, kuery, start, end] + ); + + const hasData = + (data?.allSpansDistribution.overallHistogram?.length ?? 0) > 0 || + (data?.failedSpansDistribution.overallHistogram?.length ?? 0) > 0; + + const chartData: DurationDistributionChartData[] = [ + { + areaSeriesColor: euiTheme.eui.euiColorVis1, + histogram: data?.allSpansDistribution.overallHistogram ?? [], + id: i18n.translate( + 'xpack.apm.backendOperationDistributionChart.allSpansLegendLabel', + { + defaultMessage: 'All spans', + } + ), + }, + { + areaSeriesColor: euiTheme.eui.euiColorVis7, + histogram: data?.failedSpansDistribution?.overallHistogram ?? [], + id: i18n.translate( + 'xpack.apm.backendOperationDistributionChart.failedSpansLegendLabel', + { + defaultMessage: 'Failed spans', + } + ), + }, + ]; + + const percentileThresholdValue = + data?.allSpansDistribution.percentileThresholdValue; + + return ( + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/backend_operation_detail_view/index.tsx b/x-pack/plugins/apm/public/components/app/backend_operation_detail_view/index.tsx index 8b3437402076c..9e82f4a71ee69 100644 --- a/x-pack/plugins/apm/public/components/app/backend_operation_detail_view/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_operation_detail_view/index.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; @@ -13,6 +13,7 @@ import { useApmRouter } from '../../../hooks/use_apm_router'; import { useBackendDetailOperationsBreadcrumb } from '../../../hooks/use_backend_detail_operations_breadcrumb'; import { BackendMetricCharts } from '../../shared/backend_metric_charts'; import { DetailViewHeader } from '../../shared/detail_view_header'; +import { BackendOperationDistributionChart } from './backend_operation_distribution_chart'; import { BackendOperationDetailTraceList } from './backend_operation_detail_trace_list'; export function BackendOperationDetailView() { @@ -43,7 +44,14 @@ export function BackendOperationDetailView() { - + + + + + + + + ); diff --git a/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx b/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx index 72842c9c6dcf6..aa5547fc03967 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx @@ -185,15 +185,18 @@ export function TopValues({ fieldName !== undefined && fieldValue !== undefined ) { - return callApmApi('GET /internal/apm/correlations/field_value_stats', { - params: { - query: { - ...params, - fieldName, - fieldValue, + return callApmApi( + 'GET /internal/apm/correlations/field_value_stats/transactions', + { + params: { + query: { + ...params, + fieldName, + fieldValue, + }, }, - }, - }); + } + ); } }, [params, fieldName, fieldValue, idxToHighlight] diff --git a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx index a330b08c0ad6f..8a7d69677296f 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx @@ -45,7 +45,7 @@ import { CorrelationsTable } from './correlations_table'; import { FailedTransactionsCorrelationsHelpPopover } from './failed_transactions_correlations_help_popover'; import { getFailedTransactionsCorrelationImpactLabel } from './utils/get_failed_transactions_correlation_impact_label'; import { getOverallHistogram } from './utils/get_overall_histogram'; -import { TransactionDistributionChart } from '../../shared/charts/transaction_distribution_chart'; +import { DurationDistributionChart } from '../../shared/charts/duration_distribution_chart'; import { CorrelationsEmptyStatePrompt } from './empty_state_prompt'; import { CrossClusterSearchCompatibilityWarning } from './cross_cluster_search_warning'; import { CorrelationsProgressControls } from './progress_controls'; @@ -55,7 +55,8 @@ import { OnAddFilter } from './context_popover/top_values'; import { useFailedTransactionsCorrelations } from './use_failed_transactions_correlations'; import { getTransactionDistributionChartData } from './get_transaction_distribution_chart_data'; import { ChartTitleToolTip } from './chart_title_tool_tip'; -import { MIN_TAB_TITLE_HEIGHT } from '../transaction_details/distribution'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { MIN_TAB_TITLE_HEIGHT } from '../../shared/charts/duration_distribution_chart_with_scrubber'; export function FailedTransactionsCorrelations({ onFilter, @@ -489,11 +490,12 @@ export function FailedTransactionsCorrelations({ - diff --git a/x-pack/plugins/apm/public/components/app/correlations/get_transaction_distribution_chart_data.ts b/x-pack/plugins/apm/public/components/app/correlations/get_transaction_distribution_chart_data.ts index eafead65fb77f..b2f2d019d8f43 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/get_transaction_distribution_chart_data.ts +++ b/x-pack/plugins/apm/public/components/app/correlations/get_transaction_distribution_chart_data.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { EuiTheme } from '@kbn/kibana-react-plugin/common'; import type { HistogramItem } from '../../../../common/correlations/types'; -import { TransactionDistributionChartData } from '../../shared/charts/transaction_distribution_chart'; +import { DurationDistributionChartData } from '../../shared/charts/duration_distribution_chart'; import { LatencyCorrelation } from '../../../../common/correlations/latency_correlations/types'; import { FailedTransactionsCorrelation } from '../../../../common/correlations/failed_transactions_correlations/types'; @@ -23,8 +23,7 @@ export function getTransactionDistributionChartData({ failedTransactionsHistogram?: HistogramItem[]; selectedTerm?: LatencyCorrelation | FailedTransactionsCorrelation | undefined; }) { - const transactionDistributionChartData: TransactionDistributionChartData[] = - []; + const transactionDistributionChartData: DurationDistributionChartData[] = []; if (Array.isArray(allTransactionsHistogram)) { transactionDistributionChartData.push({ diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx index f4efe77b0ca6d..d3138be96c99b 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx @@ -34,7 +34,7 @@ import { FieldStats } from '../../../../common/correlations/field_stats_types'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { TransactionDistributionChart } from '../../shared/charts/transaction_distribution_chart'; +import { DurationDistributionChart } from '../../shared/charts/duration_distribution_chart'; import { push } from '../../shared/links/url_helpers'; import { CorrelationsTable } from './correlations_table'; @@ -49,8 +49,9 @@ import { useLatencyCorrelations } from './use_latency_correlations'; import { getTransactionDistributionChartData } from './get_transaction_distribution_chart_data'; import { useTheme } from '../../../hooks/use_theme'; import { ChartTitleToolTip } from './chart_title_tool_tip'; -import { MIN_TAB_TITLE_HEIGHT } from '../transaction_details/distribution'; import { getLatencyCorrelationImpactLabel } from './utils/get_failed_transactions_correlation_impact_label'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { MIN_TAB_TITLE_HEIGHT } from '../../shared/charts/duration_distribution_chart_with_scrubber'; export function FallbackCorrelationBadge() { return ( @@ -347,11 +348,12 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { - diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.test.tsx b/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.test.tsx index 700339bfbb2af..f210af92c44ad 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.test.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.test.tsx @@ -20,6 +20,8 @@ import { delay } from '../../../utils/test_helpers'; import { fromQuery } from '../../shared/links/url_helpers'; import { useFailedTransactionsCorrelations } from './use_failed_transactions_correlations'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { APIEndpoint } from '../../../../server'; function wrapper({ children, @@ -28,53 +30,56 @@ function wrapper({ children?: ReactNode; error: boolean; }) { - const httpMethodMock = jest.fn().mockImplementation(async (endpoint) => { - await delay(100); - if (error) { - throw new Error('Something went wrong'); - } - switch (endpoint) { - case '/internal/apm/latency/overall_distribution': - return { - overallHistogram: [{ key: 'the-key', doc_count: 1234 }], - percentileThresholdValue: 1.234, - }; - case '/internal/apm/correlations/field_candidates': - return { fieldCandidates: ['field-1', 'field2'] }; - case '/internal/apm/correlations/field_value_pairs': - return { - fieldValuePairs: [ - { fieldName: 'field-name-1', fieldValue: 'field-value-1' }, - ], - }; - case '/internal/apm/correlations/p_values': - return { - failedTransactionsCorrelations: [ - { - fieldName: 'field-name-1', - fieldValue: 'field-value-1', - doc_count: 123, - bg_count: 1234, - score: 0.66, - pValue: 0.01, - normalizedScore: 0.85, - failurePercentage: 30, - successPercentage: 70, - histogram: [{ key: 'the-key', doc_count: 123 }], - }, - ], - }; - case '/internal/apm/correlations/field_stats': - return { - stats: [ - { fieldName: 'field-name-1', count: 123 }, - { fieldName: 'field-name-2', count: 1111 }, - ], - }; - default: - return {}; - } - }); + const getHttpMethodMock = (method: 'GET' | 'POST') => + jest.fn().mockImplementation(async (pathname) => { + await delay(100); + if (error) { + throw new Error('Something went wrong'); + } + const endpoint = `${method} ${pathname}` as APIEndpoint; + + switch (endpoint) { + case 'POST /internal/apm/latency/overall_distribution/transactions': + return { + overallHistogram: [{ key: 'the-key', doc_count: 1234 }], + percentileThresholdValue: 1.234, + }; + case 'GET /internal/apm/correlations/field_candidates/transactions': + return { fieldCandidates: ['field-1', 'field2'] }; + case 'POST /internal/apm/correlations/field_value_pairs/transactions': + return { + fieldValuePairs: [ + { fieldName: 'field-name-1', fieldValue: 'field-value-1' }, + ], + }; + case 'POST /internal/apm/correlations/p_values/transactions': + return { + failedTransactionsCorrelations: [ + { + fieldName: 'field-name-1', + fieldValue: 'field-value-1', + doc_count: 123, + bg_count: 1234, + score: 0.66, + pValue: 0.01, + normalizedScore: 0.85, + failurePercentage: 30, + successPercentage: 70, + histogram: [{ key: 'the-key', doc_count: 123 }], + }, + ], + }; + case 'POST /internal/apm/correlations/field_stats/transactions': + return { + stats: [ + { fieldName: 'field-name-1', count: 123 }, + { fieldName: 'field-name-2', count: 1111 }, + ], + }; + default: + return {}; + } + }); const history = createMemoryHistory(); jest.spyOn(history, 'push'); @@ -90,7 +95,9 @@ function wrapper({ }); const mockPluginContext = merge({}, mockApmPluginContextValue, { - core: { http: { get: httpMethodMock, post: httpMethodMock } }, + core: { + http: { get: getHttpMethodMock('GET'), post: getHttpMethodMock('POST') }, + }, }) as unknown as ApmPluginContextValue; return ( diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.ts b/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.ts index e95eca4cf0417..865f1d094c552 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.ts +++ b/x-pack/plugins/apm/public/components/app/correlations/use_failed_transactions_correlations.ts @@ -83,30 +83,36 @@ export function useFailedTransactionsCorrelations() { const [overallHistogramResponse, errorHistogramRespone] = await Promise.all([ // Initial call to fetch the overall distribution for the log-log plot. - callApmApi('POST /internal/apm/latency/overall_distribution', { - signal: abortCtrl.current.signal, - params: { - body: { - ...fetchParams, - percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + callApmApi( + 'POST /internal/apm/latency/overall_distribution/transactions', + { + signal: abortCtrl.current.signal, + params: { + body: { + ...fetchParams, + percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + }, }, - }, - }), - callApmApi('POST /internal/apm/latency/overall_distribution', { - signal: abortCtrl.current.signal, - params: { - body: { - ...fetchParams, - percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, - termFilters: [ - { - fieldName: EVENT_OUTCOME, - fieldValue: EventOutcome.failure, - }, - ], + } + ), + callApmApi( + 'POST /internal/apm/latency/overall_distribution/transactions', + { + signal: abortCtrl.current.signal, + params: { + body: { + ...fetchParams, + percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + termFilters: [ + { + fieldName: EVENT_OUTCOME, + fieldValue: EventOutcome.failure, + }, + ], + }, }, - }, - }), + } + ), ]); const { overallHistogram, percentileThresholdValue } = @@ -128,7 +134,7 @@ export function useFailedTransactionsCorrelations() { setResponse.flush(); const { fieldCandidates: candidates } = await callApmApi( - 'GET /internal/apm/correlations/field_candidates', + 'GET /internal/apm/correlations/field_candidates/transactions', { signal: abortCtrl.current.signal, params: { @@ -159,7 +165,7 @@ export function useFailedTransactionsCorrelations() { for (const fieldCandidatesChunk of fieldCandidatesChunks) { const pValues = await callApmApi( - 'POST /internal/apm/correlations/p_values', + 'POST /internal/apm/correlations/p_values/transactions', { signal: abortCtrl.current.signal, params: { @@ -213,7 +219,7 @@ export function useFailedTransactionsCorrelations() { setResponse.flush(); const { stats } = await callApmApi( - 'POST /internal/apm/correlations/field_stats', + 'POST /internal/apm/correlations/field_stats/transactions', { signal: abortCtrl.current.signal, params: { diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.test.tsx b/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.test.tsx index 711577cecd6ef..abf0a54970195 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.test.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.test.tsx @@ -20,6 +20,8 @@ import { delay } from '../../../utils/test_helpers'; import { fromQuery } from '../../shared/links/url_helpers'; import { useLatencyCorrelations } from './use_latency_correlations'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { APIEndpoint } from '../../../../server'; function wrapper({ children, @@ -28,48 +30,50 @@ function wrapper({ children?: ReactNode; error: boolean; }) { - const httpMethodMock = jest.fn().mockImplementation(async (endpoint) => { - await delay(100); - if (error) { - throw new Error('Something went wrong'); - } - switch (endpoint) { - case '/internal/apm/latency/overall_distribution': - return { - overallHistogram: [{ key: 'the-key', doc_count: 1234 }], - percentileThresholdValue: 1.234, - }; - case '/internal/apm/correlations/field_candidates': - return { fieldCandidates: ['field-1', 'field2'] }; - case '/internal/apm/correlations/field_value_pairs': - return { - fieldValuePairs: [ - { fieldName: 'field-name-1', fieldValue: 'field-value-1' }, - ], - }; - case '/internal/apm/correlations/significant_correlations': - return { - latencyCorrelations: [ - { - fieldName: 'field-name-1', - fieldValue: 'field-value-1', - correlation: 0.5, - histogram: [{ key: 'the-key', doc_count: 123 }], - ksTest: 0.001, - }, - ], - }; - case '/internal/apm/correlations/field_stats': - return { - stats: [ - { fieldName: 'field-name-1', count: 123 }, - { fieldName: 'field-name-2', count: 1111 }, - ], - }; - default: - return {}; - } - }); + const getHttpMethodMock = (method: 'GET' | 'POST') => + jest.fn().mockImplementation(async (pathname) => { + await delay(100); + if (error) { + throw new Error('Something went wrong'); + } + const endpoint = `${method} ${pathname}` as APIEndpoint; + switch (endpoint) { + case 'POST /internal/apm/latency/overall_distribution/transactions': + return { + overallHistogram: [{ key: 'the-key', doc_count: 1234 }], + percentileThresholdValue: 1.234, + }; + case 'GET /internal/apm/correlations/field_candidates/transactions': + return { fieldCandidates: ['field-1', 'field2'] }; + case 'POST /internal/apm/correlations/field_value_pairs/transactions': + return { + fieldValuePairs: [ + { fieldName: 'field-name-1', fieldValue: 'field-value-1' }, + ], + }; + case 'POST /internal/apm/correlations/significant_correlations/transactions': + return { + latencyCorrelations: [ + { + fieldName: 'field-name-1', + fieldValue: 'field-value-1', + correlation: 0.5, + histogram: [{ key: 'the-key', doc_count: 123 }], + ksTest: 0.001, + }, + ], + }; + case 'POST /internal/apm/correlations/field_stats/transactions': + return { + stats: [ + { fieldName: 'field-name-1', count: 123 }, + { fieldName: 'field-name-2', count: 1111 }, + ], + }; + default: + return {}; + } + }); const history = createMemoryHistory(); jest.spyOn(history, 'push'); @@ -85,7 +89,9 @@ function wrapper({ }); const mockPluginContext = merge({}, mockApmPluginContextValue, { - core: { http: { get: httpMethodMock, post: httpMethodMock } }, + core: { + http: { get: getHttpMethodMock('GET'), post: getHttpMethodMock('POST') }, + }, }) as unknown as ApmPluginContextValue; return ( diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.ts b/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.ts index 910dad59e4e61..75f5cd35a18d0 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.ts +++ b/x-pack/plugins/apm/public/components/app/correlations/use_latency_correlations.ts @@ -82,7 +82,7 @@ export function useLatencyCorrelations() { // Initial call to fetch the overall distribution for the log-log plot. const { overallHistogram, percentileThresholdValue } = await callApmApi( - 'POST /internal/apm/latency/overall_distribution', + 'POST /internal/apm/latency/overall_distribution/transactions', { signal: abortCtrl.current.signal, params: { @@ -107,7 +107,7 @@ export function useLatencyCorrelations() { setResponse.flush(); const { fieldCandidates } = await callApmApi( - 'GET /internal/apm/correlations/field_candidates', + 'GET /internal/apm/correlations/field_candidates/transactions', { signal: abortCtrl.current.signal, params: { @@ -133,7 +133,7 @@ export function useLatencyCorrelations() { for (const fieldCandidateChunk of fieldCandidateChunks) { const fieldValuePairChunkResponse = await callApmApi( - 'POST /internal/apm/correlations/field_value_pairs', + 'POST /internal/apm/correlations/field_value_pairs/transactions', { signal: abortCtrl.current.signal, params: { @@ -180,7 +180,7 @@ export function useLatencyCorrelations() { const fallbackResults: LatencyCorrelation[] = []; for (const fieldValuePairChunk of fieldValuePairChunks) { const significantCorrelations = await callApmApi( - 'POST /internal/apm/correlations/significant_correlations', + 'POST /internal/apm/correlations/significant_correlations/transactions', { signal: abortCtrl.current.signal, params: { @@ -240,7 +240,7 @@ export function useLatencyCorrelations() { setResponse.flush(); const { stats } = await callApmApi( - 'POST /internal/apm/correlations/field_stats', + 'POST /internal/apm/correlations/field_stats/transactions', { signal: abortCtrl.current.signal, params: { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx index 03132d482f374..1624bda055390 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx @@ -23,7 +23,7 @@ import { import * as useFetcherModule from '../../../../hooks/use_fetcher'; import { fromQuery } from '../../../shared/links/url_helpers'; -import { getFormattedSelection, TransactionDistribution } from '.'; +import { TransactionDistribution } from '.'; function Wrapper({ children }: { children?: ReactNode }) { const KibanaReactContext = createKibanaReactContext({ @@ -75,18 +75,6 @@ function Wrapper({ children }: { children?: ReactNode }) { } describe('transaction_details/distribution', () => { - describe('getFormattedSelection', () => { - it('displays only one unit if from and to share the same unit', () => { - expect(getFormattedSelection([10000, 100000])).toEqual('10 - 100 ms'); - }); - - it('displays two units when from and to have different units', () => { - expect(getFormattedSelection([100000, 1000000000])).toEqual( - '100 ms - 17 min' - ); - }); - }); - describe('TransactionDistribution', () => { it('shows loading indicator when the service is running and returned no results yet', async () => { jest.spyOn(useFetcherModule, 'useFetcher').mockImplementation(() => ({ diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx index 35d25e9ab406d..64525bd4fd9f6 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx @@ -5,65 +5,32 @@ * 2.0. */ -import { BrushEndListener, XYBrushEvent } from '@elastic/charts'; -import { - EuiBadge, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; +import { XYBrushEvent } from '@elastic/charts'; +import { EuiSpacer } from '@elastic/eui'; import React from 'react'; import { useHistory } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; - -import { useUiTracker } from '@kbn/observability-plugin/public'; - -import { getDurationFormatter } from '../../../../../common/utils/formatters'; - import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; -import { TransactionDistributionChart } from '../../../shared/charts/transaction_distribution_chart'; - import type { TabContentProps } from '../types'; import { useWaterfallFetcher } from '../use_waterfall_fetcher'; import { WaterfallWithSummary } from '../waterfall_with_summary'; +import { ProcessorEvent } from '../../../../../common/processor_event'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useApmParams } from '../../../../hooks/use_apm_params'; import { useTimeRange } from '../../../../hooks/use_time_range'; +import { DurationDistributionChartWithScrubber } from '../../../shared/charts/duration_distribution_chart_with_scrubber'; import { HeightRetainer } from '../../../shared/height_retainer'; import { fromQuery, toQuery } from '../../../shared/links/url_helpers'; -import { ChartTitleToolTip } from '../../correlations/chart_title_tool_tip'; -import { useTransactionDistributionChartData } from './use_transaction_distribution_chart_data'; import { TransactionTab } from '../waterfall_with_summary/transaction_tabs'; - -// Enforce min height so it's consistent across all tabs on the same level -// to prevent "flickering" behavior -export const MIN_TAB_TITLE_HEIGHT = 56; - -type Selection = [number, number]; - -// Format the selected latency range for the "Clear selection" badge. -// If the two values share the same unit, it will only displayed once. -// For example: 12 - 23 ms / 12 ms - 3 s -export function getFormattedSelection(selection: Selection): string { - const from = getDurationFormatter(selection[0])(selection[0]); - const to = getDurationFormatter(selection[1])(selection[1]); - - return `${from.unit === to.unit ? from.value : from.formatted} - ${ - to.formatted - }`; -} +import { useTransactionDistributionChartData } from './use_transaction_distribution_chart_data'; interface TransactionDistributionProps { onChartSelection: (event: XYBrushEvent) => void; onClearSelection: () => void; - selection?: Selection; + selection?: [number, number]; traceSamples: TabContentProps['traceSamples']; traceSamplesStatus: FETCH_STATUS; } @@ -102,119 +69,25 @@ export function TransactionDistribution({ waterfallStatus === FETCH_STATUS.LOADING || traceSamplesStatus === FETCH_STATUS.LOADING; - const markerCurrentTransaction = + const markerCurrentEvent = waterfall.entryWaterfallTransaction?.doc.transaction.duration.us; - const emptySelectionText = i18n.translate( - 'xpack.apm.transactionDetails.emptySelectionText', - { - defaultMessage: 'Click and drag to select a range', - } - ); - - const clearSelectionAriaLabel = i18n.translate( - 'xpack.apm.transactionDetails.clearSelectionAriaLabel', - { - defaultMessage: 'Clear selection', - } - ); - - const trackApmEvent = useUiTracker({ app: 'apm' }); - - const onTrackedChartSelection = (brushEvent: XYBrushEvent) => { - onChartSelection(brushEvent); - trackApmEvent({ metric: 'transaction_distribution_chart_selection' }); - }; - - const onTrackedClearSelection = () => { - onClearSelection(); - trackApmEvent({ metric: 'transaction_distribution_chart_clear_selection' }); - }; - const { chartData, hasData, percentileThresholdValue, status } = useTransactionDistributionChartData(); return (
- - - -
- {i18n.translate( - 'xpack.apm.transactionDetails.distribution.panelTitle', - { - defaultMessage: 'Latency distribution', - } - )} -
-
-
- - - - - - - - {selection ? ( - - - {i18n.translate( - 'xpack.apm.transactionDetails.distribution.selectionText', - { - defaultMessage: `Selection: {formattedSelection}`, - values: { - formattedSelection: getFormattedSelection(selection), - }, - } - )} - - - ) : ( - <> - - - - - {emptySelectionText} - - - )} - - -
- - - - diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/use_transaction_distribution_chart_data.ts b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/use_transaction_distribution_chart_data.ts index ae5763749a272..b343f644c8f99 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/use_transaction_distribution_chart_data.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/use_transaction_distribution_chart_data.ts @@ -37,14 +37,17 @@ export const useTransactionDistributionChartData = () => { params.start && params.end ) { - return callApmApi('POST /internal/apm/latency/overall_distribution', { - params: { - body: { - ...params, - percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + return callApmApi( + 'POST /internal/apm/latency/overall_distribution/transactions', + { + params: { + body: { + ...params, + percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + }, }, - }, - }); + } + ); } }, [params] @@ -83,20 +86,23 @@ export const useTransactionDistributionChartData = () => { params.start && params.end ) { - return callApmApi('POST /internal/apm/latency/overall_distribution', { - params: { - body: { - ...params, - percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, - termFilters: [ - { - fieldName: EVENT_OUTCOME, - fieldValue: EventOutcome.failure, - }, - ], + return callApmApi( + 'POST /internal/apm/latency/overall_distribution/transactions', + { + params: { + body: { + ...params, + percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + termFilters: [ + { + fieldName: EVENT_OUTCOME, + fieldValue: EventOutcome.failure, + }, + ], + }, }, - }, - }); + } + ); } }, [params] diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx index b0b5d495d1f8e..c8092689b9d2e 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx @@ -9,8 +9,6 @@ import React, { useCallback, useEffect, useState } from 'react'; import { omit } from 'lodash'; import { useHistory } from 'react-router-dom'; - -import { XYBrushEvent } from '@elastic/charts'; import { EuiPanel, EuiSpacer, EuiTabs, EuiTab } from '@elastic/eui'; import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params'; @@ -18,11 +16,12 @@ import { useApmParams } from '../../../hooks/use_apm_params'; import { useTransactionTraceSamplesFetcher } from '../../../hooks/use_transaction_trace_samples_fetcher'; import { maybe } from '../../../../common/utils/maybe'; -import { fromQuery, push, toQuery } from '../../shared/links/url_helpers'; +import { fromQuery, toQuery } from '../../shared/links/url_helpers'; import { failedTransactionsCorrelationsTab } from './failed_transactions_correlations_tab'; import { latencyCorrelationsTab } from './latency_correlations_tab'; import { traceSamplesTab } from './trace_samples_tab'; +import { useSampleChartSelection } from '../../../hooks/use_sample_chart_selection'; const tabs = [ traceSamplesTab, @@ -48,38 +47,11 @@ export function TransactionDetailsTabs() { environment, }); - const selectSampleFromChartSelection = (selection: XYBrushEvent) => { - if (selection !== undefined) { - const { x } = selection; - if (Array.isArray(x)) { - history.push({ - ...history.location, - search: fromQuery({ - ...toQuery(history.location.search), - sampleRangeFrom: Math.round(x[0]), - sampleRangeTo: Math.round(x[1]), - }), - }); - } - } - }; - const { sampleRangeFrom, sampleRangeTo, transactionId, traceId } = urlParams; const { traceSamples } = traceSamplesData; - const clearChartSelection = () => { - // enforces a reset of the current sample to be highlighted in the chart - // and selected in waterfall section below, otherwise we end up with - // stale data for the selected sample - push(history, { - query: { - sampleRangeFrom: '', - sampleRangeTo: '', - traceId: '', - transactionId: '', - }, - }); - }; + const { clearChartSelection, selectSampleFromChartSelection } = + useSampleChartSelection(); // When filtering in either the latency correlations or failed transactions correlations tab, // scroll to the top of the page and switch to the 'Trace samples' tab to trigger a refresh. diff --git a/x-pack/plugins/apm/public/components/routing/home/index.tsx b/x-pack/plugins/apm/public/components/routing/home/index.tsx index a4caf0f3618b9..3bbd3098788c3 100644 --- a/x-pack/plugins/apm/public/components/routing/home/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/home/index.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { Outlet, Route } from '@kbn/typed-react-router-config'; import * as t from 'io-ts'; import React, { ComponentProps } from 'react'; -import { toBooleanRt } from '@kbn/io-ts-utils'; +import { toBooleanRt, toNumberRt } from '@kbn/io-ts-utils'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { environmentRt } from '../../../../common/environment_rt'; import { TraceSearchType } from '../../../../common/trace_explorer'; @@ -284,9 +284,15 @@ export const home = { '/backends/operation': { params: t.type({ - query: t.type({ - spanName: t.string, - }), + query: t.intersection([ + t.type({ + spanName: t.string, + }), + t.partial({ + sampleRangeFrom: toNumberRt, + sampleRangeTo: toNumberRt, + }), + ]), }), element: , }, diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/duration_distribution_chart/index.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.test.tsx rename to x-pack/plugins/apm/public/components/shared/charts/duration_distribution_chart/index.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/duration_distribution_chart/index.tsx similarity index 84% rename from x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/duration_distribution_chart/index.tsx index edc55596adb4d..03e2af308a798 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/duration_distribution_chart/index.tsx @@ -39,21 +39,33 @@ import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { useTheme } from '../../../../hooks/use_theme'; import { ChartContainer } from '../chart_container'; +import { ProcessorEvent } from '../../../../../common/processor_event'; -export interface TransactionDistributionChartData { +const NUMBER_OF_TRANSACTIONS_LABEL = i18n.translate( + 'xpack.apm.durationDistribution.chart.numberOfTransactionsLabel', + { defaultMessage: 'Transactions' } +); + +const NUMBER_OF_SPANS_LABEL = i18n.translate( + 'xpack.apm.durationDistribution.chart.numberOfSpansLabel', + { defaultMessage: 'Spans' } +); + +export interface DurationDistributionChartData { id: string; histogram: HistogramItem[]; areaSeriesColor: string; } -interface TransactionDistributionChartProps { - data: TransactionDistributionChartData[]; +interface DurationDistributionChartProps { + data: DurationDistributionChartData[]; hasData: boolean; - markerCurrentTransaction?: number; + markerCurrentEvent?: number; markerValue: number; onChartSelection?: BrushEndListener; selection?: [number, number]; status: FETCH_STATUS; + eventType: ProcessorEvent.span | ProcessorEvent.transaction; } const getAnnotationsStyle = (color = 'gray'): LineAnnotationStyle => ({ @@ -93,15 +105,16 @@ export const replaceHistogramDotsWithBars = (histogramItems: HistogramItem[]) => const xAxisTickFormat: TickFormatter = (d) => getDurationFormatter(d, 0.9999)(d).formatted; -export function TransactionDistributionChart({ +export function DurationDistributionChart({ data, hasData, - markerCurrentTransaction, + markerCurrentEvent, markerValue, onChartSelection, selection, status, -}: TransactionDistributionChartProps) { + eventType, +}: DurationDistributionChartProps) { const chartTheme = useChartTheme(); const euiTheme = useTheme(); const markerPercentile = DEFAULT_PERCENTILE_THRESHOLD; @@ -110,7 +123,7 @@ export function TransactionDistributionChart({ { dataValue: markerValue, details: i18n.translate( - 'xpack.apm.transactionDistribution.chart.percentileMarkerLabel', + 'xpack.apm.durationDistribution.chart.percentileMarkerLabel', { defaultMessage: '{markerPercentile}th percentile', values: { @@ -198,15 +211,15 @@ export function TransactionDistributionChart({ hideTooltips={true} /> )} - {typeof markerCurrentTransaction === 'number' && ( + {typeof markerCurrentEvent === 'number' && ( { + it('displays only one unit if from and to share the same unit', () => { + expect(getFormattedSelection([10000, 100000])).toEqual('10 - 100 ms'); + }); + + it('displays two units when from and to have different units', () => { + expect(getFormattedSelection([100000, 1000000000])).toEqual( + '100 ms - 17 min' + ); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/charts/duration_distribution_chart_with_scrubber/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/duration_distribution_chart_with_scrubber/index.tsx new file mode 100644 index 0000000000000..161930ed87c95 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/duration_distribution_chart_with_scrubber/index.tsx @@ -0,0 +1,180 @@ +/* + * 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 { BrushEndListener, BrushEvent, XYBrushEvent } from '@elastic/charts'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useUiTracker } from '@kbn/observability-plugin/public'; +import { + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { ChartTitleToolTip } from '../../../app/correlations/chart_title_tool_tip'; +import { getDurationFormatter } from '../../../../../common/utils/formatters'; +import { + DurationDistributionChart, + DurationDistributionChartData, +} from '../duration_distribution_chart'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { ProcessorEvent } from '../../../../../common/processor_event'; + +// Format the selected latency range for the "Clear selection" badge. +// If the two values share the same unit, it will only displayed once. +// For example: 12 - 23 ms / 12 ms - 3 s +export function getFormattedSelection(selection: [number, number]): string { + const from = getDurationFormatter(selection[0])(selection[0]); + const to = getDurationFormatter(selection[1])(selection[1]); + + return `${from.unit === to.unit ? from.value : from.formatted} - ${ + to.formatted + }`; +} + +// Enforce min height so it's consistent across all tabs on the same level +// to prevent "flickering" behavior +export const MIN_TAB_TITLE_HEIGHT = 56; + +export function DurationDistributionChartWithScrubber({ + onClearSelection, + onChartSelection, + selection, + status, + markerCurrentEvent, + percentileThresholdValue, + chartData, + hasData, + eventType, +}: { + onClearSelection: () => void; + onChartSelection: (event: XYBrushEvent) => void; + selection?: [number, number]; + status: FETCH_STATUS; + markerCurrentEvent?: number; + percentileThresholdValue?: number | null; + chartData: DurationDistributionChartData[]; + hasData: boolean; + eventType: ProcessorEvent.transaction | ProcessorEvent.span; +}) { + const emptySelectionText = i18n.translate( + 'xpack.apm.durationDistributionChartWithScrubber.emptySelectionText', + { + defaultMessage: 'Click and drag to select a range', + } + ); + + const clearSelectionAriaLabel = i18n.translate( + 'xpack.apm.durationDistributionChartWithScrubber.clearSelectionAriaLabel', + { + defaultMessage: 'Clear selection', + } + ); + + const trackApmEvent = useUiTracker({ app: 'apm' }); + + const onTrackedChartSelection: BrushEndListener = ( + brushEvent: BrushEvent + ) => { + onChartSelection(brushEvent as XYBrushEvent); + // metric name is transaction_x for bwc + trackApmEvent({ metric: 'transaction_distribution_chart_selection' }); + }; + + const onTrackedClearSelection = () => { + onClearSelection(); + // metric name is transaction_x for bwc + trackApmEvent({ metric: 'transaction_distribution_chart_clear_selection' }); + }; + + return ( + <> + + + +
+ {i18n.translate( + 'xpack.apm.durationDistributionChartWithScrubber.panelTitle', + { + defaultMessage: 'Latency distribution', + } + )} +
+
+
+ + + + + + + + {selection ? ( + + + {i18n.translate( + 'xpack.apm.durationDistributionChartWithScrubber.selectionText', + { + defaultMessage: `Selection: {formattedSelection}`, + values: { + formattedSelection: getFormattedSelection(selection), + }, + } + )} + + + ) : ( + <> + + + + + {emptySelectionText} + + + )} + + +
+ + + + + + ); +} diff --git a/x-pack/plugins/apm/public/hooks/use_sample_chart_selection.ts b/x-pack/plugins/apm/public/hooks/use_sample_chart_selection.ts new file mode 100644 index 0000000000000..b6753bfd89960 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_sample_chart_selection.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { XYBrushEvent } from '@elastic/charts'; +import { useHistory } from 'react-router-dom'; +import { push } from '../components/shared/links/url_helpers'; + +export function useSampleChartSelection() { + const history = useHistory(); + const selectSampleFromChartSelection = (selection: XYBrushEvent) => { + if (selection !== undefined) { + const { x } = selection; + if (Array.isArray(x)) { + push(history, { + query: { + sampleRangeFrom: Math.round(x[0]).toString(), + sampleRangeTo: Math.round(x[1]).toString(), + }, + }); + } + } + }; + + const clearChartSelection = () => { + // enforces a reset of the current sample to be highlighted in the chart + // and selected in waterfall section below, otherwise we end up with + // stale data for the selected sample + push(history, { + query: { + sampleRangeFrom: '', + sampleRangeTo: '', + traceId: '', + transactionId: '', + }, + }); + }; + + return { + selectSampleFromChartSelection, + clearChartSelection, + }; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts index f8613bf8c9e6f..a3291aa1dd7e0 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -7,6 +7,8 @@ import type { EqlSearchRequest, + FieldCapsRequest, + FieldCapsResponse, TermsEnumRequest, TermsEnumResponse, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; @@ -17,6 +19,7 @@ import { InferSearchResponseOf, } from '@kbn/core/types/elasticsearch'; import { unwrapEsResponse } from '@kbn/observability-plugin/server'; +import { omit } from 'lodash'; import { Profile } from '../../../../../typings/es_schemas/ui/profile'; import { withApmSpan } from '../../../../utils/with_apm_span'; import { ProcessorEvent } from '../../../../../common/processor_event'; @@ -54,6 +57,10 @@ export type APMEventEqlSearchRequest = Omit & { apm: { events: ProcessorEvent[] }; }; +export type APMEventFieldCapsRequest = Omit & { + apm: { events: ProcessorEvent[] }; +}; + // These keys shoul all be `ProcessorEvent.x`, but until TypeScript 4.2 we're inlining them here. // See https://github.com/microsoft/TypeScript/issues/37888 type TypeOfProcessorEvent = { @@ -95,6 +102,48 @@ export class APMEventClient { this.includeFrozen = config.options.includeFrozen; } + private callAsyncWithDebug({ + requestType, + params, + cb, + operationName, + }: { + requestType: string; + params: Record; + cb: (requestOpts: { signal: AbortSignal; meta: true }) => Promise; + operationName: string; + }): Promise { + return callAsyncWithDebug({ + getDebugMessage: () => ({ + body: getDebugBody({ + params, + requestType, + operationName, + }), + title: getDebugTitle(this.request), + }), + isCalledWithInternalUser: false, + debug: this.debug, + request: this.request, + requestType, + operationName, + requestParams: params, + cb: () => { + const controller = new AbortController(); + + const promise = withApmSpan(operationName, () => { + return cancelEsRequestOnAbort( + cb({ signal: controller.signal, meta: true }), + this.request, + controller + ); + }); + + return unwrapEsResponse(promise); + }, + }); + } + async search( operationName: string, params: TParams @@ -111,81 +160,49 @@ export class APMEventClient { preference: 'any', }; - // only "search" operation is currently supported - const requestType = 'search'; - - return callAsyncWithDebug({ - cb: () => { - const searchPromise = withApmSpan(operationName, () => { - const controller = new AbortController(); - return cancelEsRequestOnAbort( - this.esClient.search(searchParams, { - signal: controller.signal, - meta: true, - }) as Promise, - this.request, - controller - ); - }); - - return unwrapEsResponse(searchPromise); - }, - getDebugMessage: () => ({ - body: getDebugBody({ - params: searchParams, - requestType, - operationName, - }), - title: getDebugTitle(this.request), - }), - isCalledWithInternalUser: false, - debug: this.debug, - request: this.request, - requestType, + return this.callAsyncWithDebug({ + cb: (opts) => + this.esClient.search(searchParams, opts) as unknown as Promise<{ + body: TypedSearchResponse; + }>, operationName, - requestParams: searchParams, + params: searchParams, + requestType: 'search', }); } async eqlSearch(operationName: string, params: APMEventEqlSearchRequest) { - const requestType = 'eql_search'; const index = processorEventsToIndex(params.apm.events, this.indices); - return callAsyncWithDebug({ - cb: () => { - const { apm, ...rest } = params; + const requestParams = { + index, + ...omit(params, 'apm'), + }; - const eqlSearchPromise = withApmSpan(operationName, () => { - const controller = new AbortController(); - return cancelEsRequestOnAbort( - this.esClient.eql.search( - { - index, - ...rest, - }, - { signal: controller.signal, meta: true } - ), - this.request, - controller - ); - }); + return this.callAsyncWithDebug({ + operationName, + requestType: 'eql_search', + params: requestParams, + cb: (opts) => this.esClient.eql.search(requestParams, opts), + }); + } - return unwrapEsResponse(eqlSearchPromise); - }, - getDebugMessage: () => ({ - body: getDebugBody({ - params, - requestType, - operationName, - }), - title: getDebugTitle(this.request), - }), - isCalledWithInternalUser: false, - debug: this.debug, - request: this.request, - requestType, + async fieldCaps( + operationName: string, + params: APMEventFieldCapsRequest + ): Promise { + const index = processorEventsToIndex(params.apm.events, this.indices); + + const requestParams = { + index, + ...omit(params, 'apm'), + }; + + return this.callAsyncWithDebug({ operationName, - requestParams: params, + requestType: 'field_caps', + params: requestParams, + cb: (opts) => this.esClient.fieldCaps(requestParams, opts), }); } @@ -193,43 +210,18 @@ export class APMEventClient { operationName: string, params: APMEventESTermsEnumRequest ): Promise { - const requestType = 'terms_enum'; - const { index } = unpackProcessorEvents(params, this.indices); + const index = processorEventsToIndex(params.apm.events, this.indices); - return callAsyncWithDebug({ - cb: () => { - const { apm, ...rest } = params; - const termsEnumPromise = withApmSpan(operationName, () => { - const controller = new AbortController(); - return cancelEsRequestOnAbort( - this.esClient.termsEnum( - { - index: Array.isArray(index) ? index.join(',') : index, - ...rest, - }, - { signal: controller.signal, meta: true } - ), - this.request, - controller - ); - }); + const requestParams = { + index: Array.isArray(index) ? index.join(',') : index, + ...omit(params, 'apm'), + }; - return unwrapEsResponse(termsEnumPromise); - }, - getDebugMessage: () => ({ - body: getDebugBody({ - params, - requestType, - operationName, - }), - title: getDebugTitle(this.request), - }), - isCalledWithInternalUser: false, - debug: this.debug, - request: this.request, - requestType, + return this.callAsyncWithDebug({ operationName, - requestParams: params, + requestType: 'terms_enum', + params: requestParams, + cb: (opts) => this.esClient.termsEnum(requestParams, opts), }); } } diff --git a/x-pack/plugins/apm/server/routes/backends/get_backend_latency_distribution.ts b/x-pack/plugins/apm/server/routes/backends/get_backend_latency_distribution.ts new file mode 100644 index 0000000000000..c67b64273ae5b --- /dev/null +++ b/x-pack/plugins/apm/server/routes/backends/get_backend_latency_distribution.ts @@ -0,0 +1,84 @@ +/* + * 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 { termQuery } from '@kbn/observability-plugin/server'; +import { + EVENT_OUTCOME, + SPAN_DESTINATION_SERVICE_RESOURCE, + SPAN_NAME, +} from '../../../common/elasticsearch_fieldnames'; +import { Environment } from '../../../common/environment_rt'; +import { EventOutcome } from '../../../common/event_outcome'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { Setup } from '../../lib/helpers/setup_request'; +import { getOverallLatencyDistribution } from '../latency_distribution/get_overall_latency_distribution'; +import { OverallLatencyDistributionResponse } from '../latency_distribution/types'; + +export async function getBackendLatencyDistribution({ + setup, + backendName, + spanName, + kuery, + environment, + start, + end, + percentileThreshold, +}: { + setup: Setup; + backendName: string; + spanName: string; + kuery: string; + environment: Environment; + start: number; + end: number; + percentileThreshold: number; +}): Promise<{ + allSpansDistribution: OverallLatencyDistributionResponse; + failedSpansDistribution: OverallLatencyDistributionResponse; +}> { + const commonProps = { + eventType: ProcessorEvent.span, + setup, + start, + end, + environment, + kuery, + percentileThreshold, + }; + + const commonQuery = { + bool: { + filter: [ + ...termQuery(SPAN_NAME, spanName), + ...termQuery(SPAN_DESTINATION_SERVICE_RESOURCE, backendName), + ], + }, + }; + + const [allSpansDistribution, failedSpansDistribution] = await Promise.all([ + getOverallLatencyDistribution({ + ...commonProps, + query: commonQuery, + }), + getOverallLatencyDistribution({ + ...commonProps, + query: { + bool: { + filter: [ + commonQuery, + ...termQuery(EVENT_OUTCOME, EventOutcome.failure), + ], + }, + }, + }), + ]); + + return { + allSpansDistribution, + failedSpansDistribution, + }; +} diff --git a/x-pack/plugins/apm/server/routes/backends/get_top_backend_spans.ts b/x-pack/plugins/apm/server/routes/backends/get_top_backend_spans.ts index a96c93ae383af..e6d6b99986af9 100644 --- a/x-pack/plugins/apm/server/routes/backends/get_top_backend_spans.ts +++ b/x-pack/plugins/apm/server/routes/backends/get_top_backend_spans.ts @@ -14,6 +14,7 @@ import { import { compact, keyBy } from 'lodash'; import { AGENT_NAME, + EVENT_OUTCOME, SERVICE_ENVIRONMENT, SERVICE_NAME, SPAN_DESTINATION_SERVICE_RESOURCE, @@ -25,6 +26,7 @@ import { TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; import { Environment } from '../../../common/environment_rt'; +import { EventOutcome } from '../../../common/event_outcome'; import { ProcessorEvent } from '../../../common/processor_event'; import { environmentQuery } from '../../../common/utils/environment_query'; import { AgentName } from '../../../typings/es_schemas/ui/fields/agent'; @@ -42,6 +44,7 @@ export interface BackendSpan { transactionType?: string; transactionName?: string; duration: number; + outcome: EventOutcome; } export async function getTopBackendSpans({ @@ -52,6 +55,8 @@ export async function getTopBackendSpans({ end, environment, kuery, + sampleRangeFrom, + sampleRangeTo, }: { setup: Setup; backendName: string; @@ -60,6 +65,8 @@ export async function getTopBackendSpans({ end: number; environment: Environment; kuery: string; + sampleRangeFrom?: number; + sampleRangeTo?: number; }): Promise { const { apmEventClient } = setup; @@ -78,6 +85,18 @@ export async function getTopBackendSpans({ ...kqlQuery(kuery), ...termQuery(SPAN_DESTINATION_SERVICE_RESOURCE, backendName), ...termQuery(SPAN_NAME, spanName), + ...((sampleRangeFrom ?? 0) >= 0 && (sampleRangeTo ?? 0) > 0 + ? [ + { + range: { + [SPAN_DURATION]: { + gte: sampleRangeFrom, + lte: sampleRangeTo, + }, + }, + }, + ] + : []), ], }, }, @@ -89,6 +108,7 @@ export async function getTopBackendSpans({ SERVICE_ENVIRONMENT, AGENT_NAME, SPAN_DURATION, + EVENT_OUTCOME, '@timestamp', ], }, @@ -110,6 +130,9 @@ export async function getTopBackendSpans({ }, }, _source: [TRANSACTION_ID, TRANSACTION_TYPE, TRANSACTION_NAME], + sort: { + '@timestamp': 'desc', + }, }, }) ).hits.hits.map((hit) => hit._source); @@ -131,6 +154,7 @@ export async function getTopBackendSpans({ agentName: span.agent.name, duration: span.span.duration.us, traceId: span.trace.id, + outcome: (span.event?.outcome || EventOutcome.unknown) as EventOutcome, transactionId: transaction?.transaction.id, transactionType: transaction?.transaction.type, transactionName: transaction?.transaction.name, diff --git a/x-pack/plugins/apm/server/routes/backends/route.ts b/x-pack/plugins/apm/server/routes/backends/route.ts index 0fa80406722a9..fe14bdf27af0a 100644 --- a/x-pack/plugins/apm/server/routes/backends/route.ts +++ b/x-pack/plugins/apm/server/routes/backends/route.ts @@ -22,6 +22,8 @@ import { BackendOperation, getTopBackendOperations, } from './get_top_backend_operations'; +import { getBackendLatencyDistribution } from './get_backend_latency_distribution'; +import { OverallLatencyDistributionResponse } from '../latency_distribution/types'; import { BackendSpan, getTopBackendSpans } from './get_top_backend_spans'; const topBackendsRoute = createApmServerRoute({ @@ -511,6 +513,54 @@ const backendOperationsRoute = createApmServerRoute({ }, }); +const backendLatencyDistributionChartsRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/backends/charts/distribution', + params: t.type({ + query: t.intersection([ + t.type({ + backendName: t.string, + spanName: t.string, + percentileThreshold: toNumberRt, + }), + rangeRt, + kueryRt, + environmentRt, + ]), + }), + options: { + tags: ['access:apm'], + }, + handler: async ( + resources + ): Promise<{ + allSpansDistribution: OverallLatencyDistributionResponse; + failedSpansDistribution: OverallLatencyDistributionResponse; + }> => { + const setup = await setupRequest(resources); + const { params } = resources; + const { + backendName, + spanName, + percentileThreshold, + kuery, + environment, + start, + end, + } = params.query; + + return getBackendLatencyDistribution({ + setup, + backendName, + spanName, + percentileThreshold, + kuery, + environment, + start, + end, + }); + }, +}); + const topBackendSpansRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/backends/operations/spans', options: { @@ -522,13 +572,23 @@ const topBackendSpansRoute = createApmServerRoute({ environmentRt, kueryRt, t.type({ backendName: t.string, spanName: t.string }), + t.partial({ sampleRangeFrom: toNumberRt, sampleRangeTo: toNumberRt }), ]), }), handler: async (resources): Promise<{ spans: BackendSpan[] }> => { const setup = await setupRequest(resources); const { - query: { backendName, spanName, start, end, environment, kuery }, + query: { + backendName, + spanName, + start, + end, + environment, + kuery, + sampleRangeFrom, + sampleRangeTo, + }, } = resources.params; const spans = await getTopBackendSpans({ @@ -539,6 +599,8 @@ const topBackendSpansRoute = createApmServerRoute({ end, environment, kuery, + sampleRangeFrom, + sampleRangeTo, }); return { spans }; @@ -553,5 +615,6 @@ export const backendsRouteRepository = { ...backendThroughputChartsRoute, ...backendFailedTransactionRateChartsRoute, ...backendOperationsRoute, + ...backendLatencyDistributionChartsRoute, ...topBackendSpansRoute, }; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_correlation.ts b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_correlation.ts new file mode 100644 index 0000000000000..f076aec310c9c --- /dev/null +++ b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_correlation.ts @@ -0,0 +1,104 @@ +/* + * 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { + SPAN_DURATION, + TRANSACTION_DURATION, +} from '../../../../common/elasticsearch_fieldnames'; +import type { CommonCorrelationsQueryParams } from '../../../../common/correlations/types'; + +import { Setup } from '../../../lib/helpers/setup_request'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { getCommonCorrelationsQuery } from './get_common_correlations_query'; + +export const fetchDurationCorrelation = async ({ + setup, + eventType, + start, + end, + environment, + kuery, + query, + expectations, + ranges, + fractions, + totalDocCount, +}: CommonCorrelationsQueryParams & { + setup: Setup; + eventType: ProcessorEvent; + expectations: number[]; + ranges: estypes.AggregationsAggregationRange[]; + fractions: number[]; + totalDocCount: number; +}): Promise<{ + ranges: unknown[]; + correlation: number | null; + ksTest: number | null; +}> => { + const { apmEventClient } = setup; + + const resp = await apmEventClient.search('get_duration_correlation', { + apm: { + events: [eventType], + }, + body: { + size: 0, + query: getCommonCorrelationsQuery({ + start, + end, + environment, + kuery, + query, + }), + aggs: { + latency_ranges: { + range: { + field: + eventType === ProcessorEvent.span + ? SPAN_DURATION + : TRANSACTION_DURATION, + ranges, + }, + }, + // Pearson correlation value + duration_correlation: { + bucket_correlation: { + buckets_path: 'latency_ranges>_count', + function: { + count_correlation: { + indicator: { + fractions, + expectations, + doc_count: totalDocCount, + }, + }, + }, + }, + }, + // KS test p value = ks_test.less + ks_test: { + bucket_count_ks_test: { + fractions, + buckets_path: 'latency_ranges>_count', + alternative: ['less', 'greater', 'two_sided'], + }, + }, + }, + }, + }); + + const { aggregations } = resp; + + const result = { + ranges: aggregations?.latency_ranges.buckets ?? [], + correlation: aggregations?.duration_correlation.value ?? null, + ksTest: aggregations?.ks_test.less ?? null, + }; + + return result; +}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_correlation_with_histogram.ts b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_correlation_with_histogram.ts new file mode 100644 index 0000000000000..050fa3e8364ab --- /dev/null +++ b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_correlation_with_histogram.ts @@ -0,0 +1,101 @@ +/* + * 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import { termQuery } from '@kbn/observability-plugin/server'; +import type { + CommonCorrelationsQueryParams, + FieldValuePair, +} from '../../../../common/correlations/types'; + +import { + CORRELATION_THRESHOLD, + KS_TEST_THRESHOLD, +} from '../../../../common/correlations/constants'; + +import { ProcessorEvent } from '../../../../common/processor_event'; +import { Setup } from '../../../lib/helpers/setup_request'; +import { fetchDurationCorrelation } from './fetch_duration_correlation'; +import { fetchDurationRanges } from './fetch_duration_ranges'; + +export async function fetchDurationCorrelationWithHistogram({ + setup, + eventType, + start, + end, + environment, + kuery, + query, + expectations, + ranges, + fractions, + histogramRangeSteps, + totalDocCount, + fieldValuePair, +}: CommonCorrelationsQueryParams & { + setup: Setup; + eventType: ProcessorEvent; + expectations: number[]; + ranges: estypes.AggregationsAggregationRange[]; + fractions: number[]; + histogramRangeSteps: number[]; + totalDocCount: number; + fieldValuePair: FieldValuePair; +}) { + const queryWithFieldValuePair = { + bool: { + filter: [ + query, + ...termQuery(fieldValuePair.fieldName, fieldValuePair.fieldValue), + ], + }, + }; + + const { correlation, ksTest } = await fetchDurationCorrelation({ + setup, + eventType, + start, + end, + environment, + kuery, + query: queryWithFieldValuePair, + expectations, + fractions, + ranges, + totalDocCount, + }); + + if (correlation !== null && ksTest !== null && !isNaN(ksTest)) { + if (correlation > CORRELATION_THRESHOLD && ksTest < KS_TEST_THRESHOLD) { + const logHistogram = await fetchDurationRanges({ + setup, + eventType, + start, + end, + environment, + kuery, + query: queryWithFieldValuePair, + rangeSteps: histogramRangeSteps, + }); + return { + ...fieldValuePair, + correlation, + ksTest, + histogram: logHistogram, + }; + } else { + return { + ...fieldValuePair, + correlation, + ksTest, + }; + } + } + + return undefined; +} diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_field_candidates.ts b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_field_candidates.ts similarity index 62% rename from x-pack/plugins/apm/server/routes/correlations/queries/query_field_candidates.ts rename to x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_field_candidates.ts index b8a4ae2518ed4..323bb78806100 100644 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_field_candidates.ts +++ b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_field_candidates.ts @@ -6,12 +6,8 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - import { ES_FIELD_TYPES } from '@kbn/field-types'; - -import type { ElasticsearchClient } from '@kbn/core/server'; - -import type { CorrelationsParams } from '../../../../common/correlations/types'; +import type { CommonCorrelationsQueryParams } from '../../../../common/correlations/types'; import { FIELD_PREFIX_TO_EXCLUDE_AS_CANDIDATE, FIELDS_TO_ADD_AS_CANDIDATE, @@ -20,8 +16,9 @@ import { } from '../../../../common/correlations/constants'; import { hasPrefixToInclude } from '../../../../common/correlations/utils'; -import { getQueryWithParams } from './get_query_with_params'; -import { getRequestBase } from './get_request_base'; +import { Setup } from '../../../lib/helpers/setup_request'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { getCommonCorrelationsQuery } from './get_common_correlations_query'; const SUPPORTED_ES_FIELD_TYPES = [ ES_FIELD_TYPES.KEYWORD, @@ -38,34 +35,49 @@ export const shouldBeExcluded = (fieldName: string) => { ); }; -export const getRandomDocsRequest = ( - params: CorrelationsParams -): estypes.SearchRequest => ({ - ...getRequestBase(params), - body: { - fields: ['*'], - _source: false, - query: { - function_score: { - query: getQueryWithParams({ params }), - // @ts-ignore - random_score: {}, - }, - }, - size: POPULATED_DOC_COUNT_SAMPLE_SIZE, - }, -}); +export const fetchDurationFieldCandidates = async ({ + setup, + eventType, + query, + start, + end, + environment, + kuery, +}: CommonCorrelationsQueryParams & { + query: estypes.QueryDslQueryContainer; + setup: Setup; + eventType: ProcessorEvent.transaction | ProcessorEvent.span; +}): Promise<{ + fieldCandidates: string[]; +}> => { + const { apmEventClient } = setup; -export const fetchTransactionDurationFieldCandidates = async ( - esClient: ElasticsearchClient, - params: CorrelationsParams -): Promise<{ fieldCandidates: string[] }> => { - const { index } = params; // Get all supported fields - const respMapping = await esClient.fieldCaps({ - index, - fields: '*', - }); + const [respMapping, respRandomDoc] = await Promise.all([ + apmEventClient.fieldCaps('get_field_caps', { + apm: { + events: [eventType], + }, + fields: '*', + }), + apmEventClient.search('get_random_doc_for_field_candidate', { + apm: { + events: [eventType], + }, + body: { + fields: ['*'], + _source: false, + query: getCommonCorrelationsQuery({ + start, + end, + environment, + kuery, + query, + }), + size: POPULATED_DOC_COUNT_SAMPLE_SIZE, + }, + }), + ]); const finalFieldCandidates = new Set(FIELDS_TO_ADD_AS_CANDIDATE); const acceptableFields: Set = new Set(); @@ -86,8 +98,7 @@ export const fetchTransactionDurationFieldCandidates = async ( } }); - const resp = await esClient.search(getRandomDocsRequest(params)); - const sampledDocs = resp.hits.hits.map((d) => d.fields ?? {}); + const sampledDocs = respRandomDoc.hits.hits.map((d) => d.fields ?? {}); // Get all field names for each returned doc and flatten it // to a list of unique field names used across all docs diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_fractions.ts b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_fractions.ts new file mode 100644 index 0000000000000..5e0fe08f8bf4d --- /dev/null +++ b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_fractions.ts @@ -0,0 +1,79 @@ +/* + * 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import { CommonCorrelationsQueryParams } from '../../../../common/correlations/types'; +import { + SPAN_DURATION, + TRANSACTION_DURATION, +} from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { Setup } from '../../../lib/helpers/setup_request'; +import { getCommonCorrelationsQuery } from './get_common_correlations_query'; + +/** + * Compute the actual percentile bucket counts and actual fractions + */ +export const fetchDurationFractions = async ({ + setup, + eventType, + start, + end, + environment, + kuery, + query, + ranges, +}: CommonCorrelationsQueryParams & { + setup: Setup; + eventType: ProcessorEvent; + ranges: estypes.AggregationsAggregationRange[]; +}): Promise<{ fractions: number[]; totalDocCount: number }> => { + const { apmEventClient } = setup; + const resp = await apmEventClient.search('get_duration_fractions', { + apm: { + events: [eventType], + }, + body: { + size: 0, + query: getCommonCorrelationsQuery({ + start, + end, + environment, + kuery, + query, + }), + aggs: { + latency_ranges: { + range: { + field: + eventType === ProcessorEvent.span + ? SPAN_DURATION + : TRANSACTION_DURATION, + ranges, + }, + }, + }, + }, + }); + + const { aggregations } = resp; + + const totalDocCount = + aggregations?.latency_ranges.buckets.reduce((acc, bucket) => { + return acc + bucket.doc_count; + }, 0) ?? 0; + + // Compute (doc count per bucket/total doc count) + return { + fractions: + aggregations?.latency_ranges.buckets.map( + (bucket) => bucket.doc_count / totalDocCount + ) ?? [], + totalDocCount, + }; +}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_histogram_range_steps.ts b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_histogram_range_steps.ts new file mode 100644 index 0000000000000..349eb1a400f7b --- /dev/null +++ b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_histogram_range_steps.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { scaleLog } from 'd3-scale'; + +import { isFiniteNumber } from '@kbn/observability-plugin/common/utils/is_finite_number'; +import { CommonCorrelationsQueryParams } from '../../../../common/correlations/types'; +import { + SPAN_DURATION, + TRANSACTION_DURATION, +} from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { Setup } from '../../../lib/helpers/setup_request'; +import { getCommonCorrelationsQuery } from './get_common_correlations_query'; + +const getHistogramRangeSteps = (min: number, max: number, steps: number) => { + // A d3 based scale function as a helper to get equally distributed bins on a log scale. + // We round the final values because the ES range agg we use won't accept numbers with decimals for `transaction.duration.us`. + const logFn = scaleLog().domain([min, max]).range([1, steps]); + return [...Array(steps).keys()] + .map(logFn.invert) + .map((d) => (isNaN(d) ? 0 : Math.round(d))); +}; + +export const fetchDurationHistogramRangeSteps = async ({ + eventType, + setup, + start, + end, + environment, + kuery, + query, +}: CommonCorrelationsQueryParams & { + eventType: ProcessorEvent; + setup: Setup; +}): Promise => { + const { apmEventClient } = setup; + + const steps = 100; + + const durationField = + eventType === ProcessorEvent.span ? SPAN_DURATION : TRANSACTION_DURATION; + + const resp = await apmEventClient.search( + 'get_duration_histogram_range_steps', + { + apm: { + events: [eventType], + }, + body: { + size: 0, + query: getCommonCorrelationsQuery({ + start, + end, + environment, + kuery, + query, + }), + aggs: { + duration_min: { min: { field: durationField } }, + duration_max: { max: { field: durationField } }, + }, + }, + } + ); + + if (resp.hits.total.value === 0) { + return getHistogramRangeSteps(0, 1, 100); + } + + if ( + !resp.aggregations || + !( + isFiniteNumber(resp.aggregations.duration_min.value) && + isFiniteNumber(resp.aggregations.duration_max.value) + ) + ) { + return []; + } + + const min = resp.aggregations.duration_min.value; + const max = resp.aggregations.duration_max.value * 2; + + return getHistogramRangeSteps(min, max, steps); +}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_percentiles.ts b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_percentiles.ts new file mode 100644 index 0000000000000..706ccb72dba31 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_percentiles.ts @@ -0,0 +1,78 @@ +/* + * 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 { + SPAN_DURATION, + TRANSACTION_DURATION, +} from '../../../../common/elasticsearch_fieldnames'; +import { SIGNIFICANT_VALUE_DIGITS } from '../../../../common/correlations/constants'; +import { Setup } from '../../../lib/helpers/setup_request'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { getCommonCorrelationsQuery } from './get_common_correlations_query'; +import { CommonCorrelationsQueryParams } from '../../../../common/correlations/types'; + +export const fetchDurationPercentiles = async ({ + eventType, + setup, + start, + end, + environment, + kuery, + query, + percents, +}: CommonCorrelationsQueryParams & { + eventType: ProcessorEvent; + setup: Setup; + percents?: number[]; +}): Promise<{ + totalDocs: number; + percentiles: Record; +}> => { + const response = await setup.apmEventClient.search( + 'get_duration_percentiles', + { + apm: { events: [eventType] }, + body: { + track_total_hits: true, + query: getCommonCorrelationsQuery({ + start, + end, + environment, + kuery, + query, + }), + size: 0, + aggs: { + duration_percentiles: { + percentiles: { + hdr: { + number_of_significant_value_digits: SIGNIFICANT_VALUE_DIGITS, + }, + field: + eventType === ProcessorEvent.span + ? SPAN_DURATION + : TRANSACTION_DURATION, + ...(Array.isArray(percents) ? { percents } : {}), + }, + }, + }, + }, + } + ); + + // return early with no results if the search didn't return any documents + if (!response.aggregations || response.hits.total.value === 0) { + return { totalDocs: 0, percentiles: {} }; + } + + return { + totalDocs: response.hits.total.value, + percentiles: (response.aggregations.duration_percentiles.values ?? + // we know values won't be null because there are hits + {}) as Record, + }; +}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_ranges.ts b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_ranges.ts new file mode 100644 index 0000000000000..30dc52b1a6e93 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_duration_ranges.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { + SPAN_DURATION, + TRANSACTION_DURATION, +} from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { Setup } from '../../../lib/helpers/setup_request'; +import { getCommonCorrelationsQuery } from './get_common_correlations_query'; +import { Environment } from '../../../../common/environment_rt'; + +export const fetchDurationRanges = async ({ + rangeSteps, + setup, + start, + end, + environment, + kuery, + query, + eventType, +}: { + rangeSteps: number[]; + setup: Setup; + start: number; + end: number; + environment: Environment; + kuery: string; + query: estypes.QueryDslQueryContainer; + eventType: ProcessorEvent; +}): Promise> => { + const { apmEventClient } = setup; + + const ranges = rangeSteps.reduce( + (p, to) => { + const from = p[p.length - 1].to; + p.push({ from, to }); + return p; + }, + [{ to: 0 }] as Array<{ from?: number; to?: number }> + ); + if (ranges.length > 0) { + ranges.push({ from: ranges[ranges.length - 1].to }); + } + + const resp = await apmEventClient.search('get_duration_ranges', { + apm: { + events: [eventType], + }, + body: { + size: 0, + query: getCommonCorrelationsQuery({ + start, + end, + environment, + kuery, + query, + }), + aggs: { + logspace_ranges: { + range: { + field: + eventType === ProcessorEvent.span + ? SPAN_DURATION + : TRANSACTION_DURATION, + ranges, + }, + }, + }, + }, + }); + + return ( + resp.aggregations?.logspace_ranges.buckets + .map((d) => ({ + key: d.from, + doc_count: d.doc_count, + })) + .filter( + (d): d is { key: number; doc_count: number } => d.key !== undefined + ) ?? [] + ); +}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/fetch_failed_events_correlation_p_values.ts b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_failed_events_correlation_p_values.ts new file mode 100644 index 0000000000000..002cea455ac28 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_failed_events_correlation_p_values.ts @@ -0,0 +1,138 @@ +/* + * 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 { termQuery } from '@kbn/observability-plugin/server'; +import { CommonCorrelationsQueryParams } from '../../../../common/correlations/types'; +import { FailedTransactionsCorrelation } from '../../../../common/correlations/failed_transactions_correlations/types'; +import { + EVENT_OUTCOME, + PROCESSOR_EVENT, +} from '../../../../common/elasticsearch_fieldnames'; +import { EventOutcome } from '../../../../common/event_outcome'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { Setup } from '../../../lib/helpers/setup_request'; +import { getCommonCorrelationsQuery } from './get_common_correlations_query'; +import { fetchDurationRanges } from './fetch_duration_ranges'; + +export const fetchFailedEventsCorrelationPValues = async ({ + setup, + eventType, + start, + end, + environment, + kuery, + query, + rangeSteps, + fieldName, +}: CommonCorrelationsQueryParams & { + setup: Setup; + eventType: ProcessorEvent; + rangeSteps: number[]; + fieldName: string; +}) => { + const { apmEventClient } = setup; + + const commonQuery = getCommonCorrelationsQuery({ + start, + end, + environment, + kuery, + query, + }); + + const resp = await apmEventClient.search( + 'get_failed_events_correlation_p_values', + { + apm: { + events: [eventType], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + commonQuery, + ...termQuery(EVENT_OUTCOME, EventOutcome.failure), + ], + }, + }, + aggs: { + failure_p_value: { + significant_terms: { + field: fieldName, + background_filter: { + // Important to have same query as above here + // without it, we would be comparing sets of different filtered elements + bool: { + filter: [ + commonQuery, + ...termQuery(PROCESSOR_EVENT, eventType), + ], + }, + }, + // No need to have must_not "event.outcome": "failure" clause + // if background_is_superset is set to true + p_value: { background_is_superset: true }, + }, + }, + }, + }, + } + ); + + const { aggregations } = resp; + + if (!aggregations) { + return []; + } + + const overallResult = aggregations.failure_p_value; + + // Using for of to sequentially augment the results with histogram data. + const result: FailedTransactionsCorrelation[] = []; + for (const bucket of overallResult.buckets) { + // Scale the score into a value from 0 - 1 + // using a concave piecewise linear function in -log(p-value) + const normalizedScore = + 0.5 * Math.min(Math.max((bucket.score - 3.912) / 2.995, 0), 1) + + 0.25 * Math.min(Math.max((bucket.score - 6.908) / 6.908, 0), 1) + + 0.25 * Math.min(Math.max((bucket.score - 13.816) / 101.314, 0), 1); + + const histogram = await fetchDurationRanges({ + setup, + eventType, + start, + end, + environment, + kuery, + query: { + bool: { + filter: [query, ...termQuery(fieldName, bucket.key)], + }, + }, + rangeSteps, + }); + + result.push({ + fieldName, + fieldValue: bucket.key, + doc_count: bucket.doc_count, + bg_count: bucket.doc_count, + score: bucket.score, + pValue: Math.exp(-bucket.score), + normalizedScore, + // Percentage of time the term appears in failed transactions + failurePercentage: bucket.doc_count / overallResult.doc_count, + // Percentage of time the term appears in successful transactions + successPercentage: + (bucket.bg_count - bucket.doc_count) / + (overallResult.bg_count - overallResult.doc_count), + histogram, + }); + } + + return result; +}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/fetch_field_value_pairs.ts b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_field_value_pairs.ts new file mode 100644 index 0000000000000..e18f9d4f71e32 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_field_value_pairs.ts @@ -0,0 +1,78 @@ +/* + * 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 { + FieldValuePair, + CommonCorrelationsQueryParams, +} from '../../../../common/correlations/types'; +import { TERMS_SIZE } from '../../../../common/correlations/constants'; + +import { splitAllSettledPromises } from '../utils'; +import { Setup } from '../../../lib/helpers/setup_request'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { getCommonCorrelationsQuery } from './get_common_correlations_query'; + +export const fetchFieldValuePairs = async ({ + setup, + fieldCandidates, + eventType, + start, + end, + environment, + kuery, + query, +}: CommonCorrelationsQueryParams & { + setup: Setup; + fieldCandidates: string[]; + eventType: ProcessorEvent; +}): Promise<{ fieldValuePairs: FieldValuePair[]; errors: any[] }> => { + const { apmEventClient } = setup; + + const { fulfilled: responses, rejected: errors } = splitAllSettledPromises( + await Promise.allSettled( + fieldCandidates.map(async (fieldName) => { + const response = await apmEventClient.search( + 'get_field_value_pairs_for_field_candidate', + { + apm: { + events: [eventType], + }, + body: { + size: 0, + query: getCommonCorrelationsQuery({ + start, + end, + environment, + kuery, + query, + }), + aggs: { + attribute_terms: { + terms: { + field: fieldName, + size: TERMS_SIZE, + }, + }, + }, + }, + } + ); + + return ( + response.aggregations?.attribute_terms.buckets.map((d) => ({ + fieldName, + // The terms aggregation returns boolean fields as { key: 0, key_as_string: "false" }, + // so we need to pick `key_as_string` if it's present, otherwise searches on boolean fields would fail later on. + fieldValue: d.key_as_string ?? d.key, + })) ?? [] + ); + }) + ) + ); + + return { fieldValuePairs: responses.flat(), errors }; +}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_p_values.ts b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_p_values.ts similarity index 58% rename from x-pack/plugins/apm/server/routes/correlations/queries/query_p_values.ts rename to x-pack/plugins/apm/server/routes/correlations/queries/fetch_p_values.ts index c3c4c814cd328..4321c3d86784c 100644 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_p_values.ts +++ b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_p_values.ts @@ -5,38 +5,54 @@ * 2.0. */ -import type { ElasticsearchClient } from '@kbn/core/server'; - -import type { CorrelationsParams } from '../../../../common/correlations/types'; -import type { FailedTransactionsCorrelation } from '../../../../common/correlations/failed_transactions_correlations/types'; import { ERROR_CORRELATION_THRESHOLD } from '../../../../common/correlations/constants'; +import type { FailedTransactionsCorrelation } from '../../../../common/correlations/failed_transactions_correlations/types'; +import { CommonCorrelationsQueryParams } from '../../../../common/correlations/types'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { Setup } from '../../../lib/helpers/setup_request'; import { splitAllSettledPromises } from '../utils'; +import { fetchDurationHistogramRangeSteps } from './fetch_duration_histogram_range_steps'; +import { fetchFailedEventsCorrelationPValues } from './fetch_failed_events_correlation_p_values'; -import { - fetchFailedTransactionsCorrelationPValues, - fetchTransactionDurationHistogramRangeSteps, -} from '.'; - -export const fetchPValues = async ( - esClient: ElasticsearchClient, - paramsWithIndex: CorrelationsParams, - fieldCandidates: string[] -) => { - const histogramRangeSteps = await fetchTransactionDurationHistogramRangeSteps( - esClient, - paramsWithIndex - ); +export const fetchPValues = async ({ + setup, + eventType, + start, + end, + environment, + kuery, + query, + fieldCandidates, +}: CommonCorrelationsQueryParams & { + setup: Setup; + eventType: ProcessorEvent; + fieldCandidates: string[]; +}) => { + const rangeSteps = await fetchDurationHistogramRangeSteps({ + setup, + eventType, + start, + end, + environment, + kuery, + query, + }); const { fulfilled, rejected } = splitAllSettledPromises( await Promise.allSettled( fieldCandidates.map((fieldName) => - fetchFailedTransactionsCorrelationPValues( - esClient, - paramsWithIndex, - histogramRangeSteps, - fieldName - ) + fetchFailedEventsCorrelationPValues({ + setup, + eventType, + start, + end, + environment, + kuery, + query, + fieldName, + rangeSteps, + }) ) ) ); @@ -72,8 +88,9 @@ export const fetchPValues = async ( } }); - const ccsWarning = - rejected.length > 0 && paramsWithIndex?.index.includes(':'); + const index = setup.indices[eventType as keyof typeof setup.indices]; + + const ccsWarning = rejected.length > 0 && index.includes(':'); return { failedTransactionsCorrelations, ccsWarning, fallbackResult }; }; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_significant_correlations.ts b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_significant_correlations.ts similarity index 53% rename from x-pack/plugins/apm/server/routes/correlations/queries/query_significant_correlations.ts rename to x-pack/plugins/apm/server/routes/correlations/queries/fetch_significant_correlations.ts index c7293fc87e3d1..7ba65470803ae 100644 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_significant_correlations.ts +++ b/x-pack/plugins/apm/server/routes/correlations/queries/fetch_significant_correlations.ts @@ -7,40 +7,51 @@ import { range } from 'lodash'; -import type { ElasticsearchClient } from '@kbn/core/server'; - +import { termQuery } from '@kbn/observability-plugin/server'; +import type { LatencyCorrelation } from '../../../../common/correlations/latency_correlations/types'; import type { + CommonCorrelationsQueryParams, FieldValuePair, - CorrelationsParams, } from '../../../../common/correlations/types'; -import type { LatencyCorrelation } from '../../../../common/correlations/latency_correlations/types'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { Setup } from '../../../lib/helpers/setup_request'; import { computeExpectationsAndRanges, splitAllSettledPromises, } from '../utils'; +import { fetchDurationPercentiles } from './fetch_duration_percentiles'; +import { fetchDurationCorrelationWithHistogram } from './fetch_duration_correlation_with_histogram'; +import { fetchDurationFractions } from './fetch_duration_fractions'; +import { fetchDurationHistogramRangeSteps } from './fetch_duration_histogram_range_steps'; +import { fetchDurationRanges } from './fetch_duration_ranges'; -import { - fetchTransactionDurationCorrelationWithHistogram, - fetchTransactionDurationFractions, - fetchTransactionDurationHistogramRangeSteps, - fetchTransactionDurationPercentiles, - fetchTransactionDurationRanges, -} from '.'; - -export const fetchSignificantCorrelations = async ( - esClient: ElasticsearchClient, - paramsWithIndex: CorrelationsParams, - fieldValuePairs: FieldValuePair[] -) => { +export const fetchSignificantCorrelations = async ({ + setup, + eventType, + start, + end, + environment, + kuery, + query, + fieldValuePairs, +}: CommonCorrelationsQueryParams & { + setup: Setup; + eventType: ProcessorEvent; + fieldValuePairs: FieldValuePair[]; +}) => { // Create an array of ranges [2, 4, 6, ..., 98] const percentileAggregationPercents = range(2, 100, 2); - const { percentiles: percentilesRecords } = - await fetchTransactionDurationPercentiles( - esClient, - paramsWithIndex, - percentileAggregationPercents - ); + const { percentiles: percentilesRecords } = await fetchDurationPercentiles({ + setup, + eventType, + start, + end, + environment, + kuery, + query, + percents: percentileAggregationPercents, + }); // We need to round the percentiles values // because the queries we're using based on it @@ -49,30 +60,45 @@ export const fetchSignificantCorrelations = async ( const { expectations, ranges } = computeExpectationsAndRanges(percentiles); - const { fractions, totalDocCount } = await fetchTransactionDurationFractions( - esClient, - paramsWithIndex, - ranges - ); + const { fractions, totalDocCount } = await fetchDurationFractions({ + setup, + eventType, + start, + end, + environment, + kuery, + query, + ranges, + }); - const histogramRangeSteps = await fetchTransactionDurationHistogramRangeSteps( - esClient, - paramsWithIndex - ); + const histogramRangeSteps = await fetchDurationHistogramRangeSteps({ + setup, + eventType, + start, + end, + environment, + kuery, + query, + }); const { fulfilled, rejected } = splitAllSettledPromises( await Promise.allSettled( fieldValuePairs.map((fieldValuePair) => - fetchTransactionDurationCorrelationWithHistogram( - esClient, - paramsWithIndex, + fetchDurationCorrelationWithHistogram({ + setup, + eventType, + start, + end, + environment, + kuery, + query, expectations, ranges, fractions, histogramRangeSteps, totalDocCount, - fieldValuePair - ) + fieldValuePair, + }) ) ) ); @@ -80,6 +106,7 @@ export const fetchSignificantCorrelations = async ( const latencyCorrelations = fulfilled.filter( (d) => d && 'histogram' in d ) as LatencyCorrelation[]; + let fallbackResult: LatencyCorrelation | undefined = latencyCorrelations.length > 0 ? undefined @@ -103,12 +130,20 @@ export const fetchSignificantCorrelations = async ( }, undefined); if (latencyCorrelations.length === 0 && fallbackResult) { const { fieldName, fieldValue } = fallbackResult; - const logHistogram = await fetchTransactionDurationRanges( - esClient, - paramsWithIndex, - histogramRangeSteps, - [{ fieldName, fieldValue }] - ); + const logHistogram = await fetchDurationRanges({ + setup, + eventType, + start, + end, + environment, + kuery, + query: { + bool: { + filter: [query, ...termQuery(fieldName, fieldValue)], + }, + }, + rangeSteps: histogramRangeSteps, + }); if (fallbackResult) { fallbackResult = { @@ -118,8 +153,9 @@ export const fetchSignificantCorrelations = async ( } } - const ccsWarning = - rejected.length > 0 && paramsWithIndex?.index.includes(':'); + const index = setup.indices[eventType as keyof typeof setup.indices]; + + const ccsWarning = rejected.length > 0 && index.includes(':'); return { latencyCorrelations, diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/fetch_boolean_field_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/fetch_boolean_field_stats.ts new file mode 100644 index 0000000000000..d479a10fda45e --- /dev/null +++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/fetch_boolean_field_stats.ts @@ -0,0 +1,76 @@ +/* + * 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 { + CommonCorrelationsQueryParams, + FieldValuePair, +} from '../../../../../common/correlations/types'; +import { BooleanFieldStats } from '../../../../../common/correlations/field_stats_types'; +import { Setup } from '../../../../lib/helpers/setup_request'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { getCommonCorrelationsQuery } from '../get_common_correlations_query'; + +export const fetchBooleanFieldStats = async ({ + setup, + eventType, + start, + end, + environment, + kuery, + field, + query, +}: CommonCorrelationsQueryParams & { + setup: Setup; + eventType: ProcessorEvent; + field: FieldValuePair; +}): Promise => { + const { apmEventClient } = setup; + + const { fieldName } = field; + + const { aggregations } = await apmEventClient.search( + 'get_boolean_field_stats', + { + apm: { + events: [eventType], + }, + body: { + size: 0, + track_total_hits: false, + query: getCommonCorrelationsQuery({ + start, + end, + environment, + kuery, + query, + }), + aggs: { + sampled_value_count: { + filter: { exists: { field: fieldName } }, + }, + sampled_values: { + terms: { + field: fieldName, + size: 2, + }, + }, + }, + }, + } + ); + + const stats: BooleanFieldStats = { + fieldName: field.fieldName, + count: aggregations?.sampled_value_count.doc_count ?? 0, + }; + + const valueBuckets = aggregations?.sampled_values?.buckets ?? []; + valueBuckets.forEach((bucket) => { + stats[`${bucket.key.toString()}Count`] = bucket.doc_count; + }); + return stats; +}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/fetch_field_value_field_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/fetch_field_value_field_stats.ts new file mode 100644 index 0000000000000..b7236b6a603c7 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/fetch_field_value_field_stats.ts @@ -0,0 +1,79 @@ +/* + * 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 { + CommonCorrelationsQueryParams, + FieldValuePair, +} from '../../../../../common/correlations/types'; +import { + FieldValueFieldStats, + TopValueBucket, +} from '../../../../../common/correlations/field_stats_types'; +import { Setup } from '../../../../lib/helpers/setup_request'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { getCommonCorrelationsQuery } from '../get_common_correlations_query'; + +export const fetchFieldValueFieldStats = async ({ + setup, + eventType, + start, + end, + environment, + kuery, + query, + field, +}: CommonCorrelationsQueryParams & { + eventType: ProcessorEvent; + setup: Setup; + field: FieldValuePair; +}): Promise => { + const { apmEventClient } = setup; + + const { aggregations } = await apmEventClient.search( + 'get_field_value_field_stats', + { + apm: { + events: [eventType], + }, + body: { + size: 0, + track_total_hits: false, + query: getCommonCorrelationsQuery({ + start, + end, + environment, + kuery, + query, + }), + aggs: { + filtered_count: { + filter: { + term: { + [`${field?.fieldName}`]: field?.fieldValue, + }, + }, + }, + }, + }, + } + ); + + const topValues: TopValueBucket[] = [ + { + key: field.fieldValue, + doc_count: aggregations?.filtered_count.doc_count ?? 0, + }, + ]; + + const stats = { + fieldName: field.fieldName, + topValues, + topValuesSampleSize: aggregations?.filtered_count.doc_count ?? 0, + }; + + return stats; +}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/fetch_fields_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/fetch_fields_stats.ts new file mode 100644 index 0000000000000..05da7d77c281d --- /dev/null +++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/fetch_fields_stats.ts @@ -0,0 +1,139 @@ +/* + * 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 { chunk } from 'lodash'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; +import { rangeQuery } from '@kbn/observability-plugin/server'; +import { + CommonCorrelationsQueryParams, + FieldValuePair, +} from '../../../../../common/correlations/types'; +import { FieldStats } from '../../../../../common/correlations/field_stats_types'; +import { fetchKeywordFieldStats } from './fetch_keyword_field_stats'; +import { fetchNumericFieldStats } from './fetch_numeric_field_stats'; +import { fetchBooleanFieldStats } from './fetch_boolean_field_stats'; +import { Setup } from '../../../../lib/helpers/setup_request'; +import { ProcessorEvent } from '../../../../../common/processor_event'; + +export const fetchFieldsStats = async ({ + setup, + eventType, + start, + end, + environment, + kuery, + query, + fieldsToSample, +}: CommonCorrelationsQueryParams & { + eventType: ProcessorEvent; + setup: Setup; + fieldsToSample: string[]; +}): Promise<{ + stats: FieldStats[]; + errors: any[]; +}> => { + const { apmEventClient } = setup; + const stats: FieldStats[] = []; + const errors: any[] = []; + + if (fieldsToSample.length === 0) return { stats, errors }; + + const respMapping = await apmEventClient.fieldCaps( + 'get_field_caps_for_field_stats', + { + apm: { + events: [eventType], + }, + body: { + index_filter: { + bool: { + filter: [...rangeQuery(start, end)], + }, + }, + }, + fields: fieldsToSample, + } + ); + + const fieldStatsPromises = Object.entries(respMapping.fields) + .map(([key, value], idx) => { + const field: FieldValuePair = { fieldName: key, fieldValue: '' }; + const fieldTypes = Object.keys(value); + + for (const ft of fieldTypes) { + switch (ft) { + case ES_FIELD_TYPES.KEYWORD: + case ES_FIELD_TYPES.IP: + return fetchKeywordFieldStats({ + setup, + eventType, + start, + end, + environment, + kuery, + query, + field, + }); + break; + + case 'numeric': + case 'number': + case ES_FIELD_TYPES.FLOAT: + case ES_FIELD_TYPES.HALF_FLOAT: + case ES_FIELD_TYPES.SCALED_FLOAT: + case ES_FIELD_TYPES.DOUBLE: + case ES_FIELD_TYPES.INTEGER: + case ES_FIELD_TYPES.LONG: + case ES_FIELD_TYPES.SHORT: + case ES_FIELD_TYPES.UNSIGNED_LONG: + case ES_FIELD_TYPES.BYTE: + return fetchNumericFieldStats({ + setup, + eventType, + start, + end, + environment, + kuery, + query, + field, + }); + + break; + case ES_FIELD_TYPES.BOOLEAN: + return fetchBooleanFieldStats({ + setup, + eventType, + start, + end, + environment, + kuery, + query, + field, + }); + + default: + return; + } + } + }) + .filter((f) => f !== undefined) as Array>; + + const batches = chunk(fieldStatsPromises, 10); + for (let i = 0; i < batches.length; i++) { + try { + const results = await Promise.allSettled(batches[i]); + results.forEach((r) => { + if (r.status === 'fulfilled' && r.value !== undefined) { + stats.push(r.value); + } + }); + } catch (e) { + errors.push(e); + } + } + + return { stats, errors }; +}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/fetch_keyword_field_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/fetch_keyword_field_stats.ts new file mode 100644 index 0000000000000..6803f7a248f1f --- /dev/null +++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/fetch_keyword_field_stats.ts @@ -0,0 +1,71 @@ +/* + * 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 { + CommonCorrelationsQueryParams, + FieldValuePair, +} from '../../../../../common/correlations/types'; +import { KeywordFieldStats } from '../../../../../common/correlations/field_stats_types'; +import { Setup } from '../../../../lib/helpers/setup_request'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { getCommonCorrelationsQuery } from '../get_common_correlations_query'; + +export const fetchKeywordFieldStats = async ({ + setup, + eventType, + start, + end, + environment, + kuery, + query, + field, +}: CommonCorrelationsQueryParams & { + setup: Setup; + eventType: ProcessorEvent; + field: FieldValuePair; +}): Promise => { + const { apmEventClient } = setup; + + const body = await apmEventClient.search('get_keyword_field_stats', { + apm: { + events: [eventType], + }, + body: { + size: 0, + track_total_hits: false, + query: getCommonCorrelationsQuery({ + start, + end, + kuery, + environment, + query, + }), + aggs: { + sampled_top: { + terms: { + field: field.fieldName, + size: 10, + }, + }, + }, + }, + }); + + const aggregations = body.aggregations; + const topValues = aggregations?.sampled_top?.buckets ?? []; + + const stats = { + fieldName: field.fieldName, + topValues, + topValuesSampleSize: topValues.reduce( + (acc, curr) => acc + curr.doc_count, + aggregations?.sampled_top?.sum_other_doc_count ?? 0 + ), + }; + + return stats; +}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/fetch_numeric_field_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/fetch_numeric_field_stats.ts new file mode 100644 index 0000000000000..a2e512a30fda8 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/fetch_numeric_field_stats.ts @@ -0,0 +1,97 @@ +/* + * 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { + NumericFieldStats, + TopValueBucket, +} from '../../../../../common/correlations/field_stats_types'; +import { + CommonCorrelationsQueryParams, + FieldValuePair, +} from '../../../../../common/correlations/types'; +import { Setup } from '../../../../lib/helpers/setup_request'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { getCommonCorrelationsQuery } from '../get_common_correlations_query'; + +export const fetchNumericFieldStats = async ({ + setup, + eventType, + start, + end, + environment, + kuery, + query, + field, +}: CommonCorrelationsQueryParams & { + setup: Setup; + eventType: ProcessorEvent; + field: FieldValuePair; +}): Promise => { + const { apmEventClient } = setup; + + const { fieldName } = field; + + const { aggregations } = await apmEventClient.search( + 'get_numeric_field_stats', + { + apm: { + events: [eventType], + }, + body: { + size: 0, + track_total_hits: false, + query: getCommonCorrelationsQuery({ + start, + end, + environment, + kuery, + query, + }), + aggs: { + sampled_field_stats: { + filter: { exists: { field: fieldName } }, + aggs: { + actual_stats: { + stats: { field: fieldName }, + }, + }, + }, + sampled_top: { + terms: { + field: fieldName, + size: 10, + order: { + _count: 'desc', + }, + }, + }, + }, + }, + } + ); + + const docCount = aggregations?.sampled_field_stats?.doc_count ?? 0; + const fieldStatsResp: Partial = + aggregations?.sampled_field_stats?.actual_stats ?? {}; + const topValues = aggregations?.sampled_top?.buckets ?? []; + + const stats: NumericFieldStats = { + fieldName: field.fieldName, + count: docCount, + min: fieldStatsResp?.min || 0, + max: fieldStatsResp?.max || 0, + avg: fieldStatsResp?.avg || 0, + topValues, + topValuesSampleSize: topValues.reduce( + (acc: number, curr: TopValueBucket) => acc + curr.doc_count, + aggregations?.sampled_top?.sum_other_doc_count ?? 0 + ), + }; + + return stats; +}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_boolean_field_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_boolean_field_stats.ts deleted file mode 100644 index 4ff00f93bec80..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_boolean_field_stats.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ElasticsearchClient } from '@kbn/core/server'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { FieldValuePair } from '../../../../../common/correlations/types'; -import { - FieldStatsCommonRequestParams, - BooleanFieldStats, - Aggs, - TopValueBucket, -} from '../../../../../common/correlations/field_stats_types'; -import { getQueryWithParams } from '../get_query_with_params'; - -export const getBooleanFieldStatsRequest = ( - params: FieldStatsCommonRequestParams, - fieldName: string, - termFilters?: FieldValuePair[] -): estypes.SearchRequest => { - const query = getQueryWithParams({ params, termFilters }); - - const { index } = params; - - const size = 0; - const aggs: Aggs = { - sampled_value_count: { - filter: { exists: { field: fieldName } }, - }, - sampled_values: { - terms: { - field: fieldName, - size: 2, - }, - }, - }; - - const searchBody = { - query, - aggs, - }; - - return { - index, - size, - track_total_hits: false, - body: searchBody, - }; -}; - -interface SamplesValuesAggs - extends estypes.AggregationsTermsAggregateBase { - buckets: TopValueBucket[]; -} - -interface FieldStatsAggs { - sampled_value_count: estypes.AggregationsSingleBucketAggregateBase; - sampled_values: SamplesValuesAggs; -} - -export const fetchBooleanFieldStats = async ( - esClient: ElasticsearchClient, - params: FieldStatsCommonRequestParams, - field: FieldValuePair, - termFilters?: FieldValuePair[] -): Promise => { - const request = getBooleanFieldStatsRequest( - params, - field.fieldName, - termFilters - ); - const body = await esClient.search(request); - const aggregations = body.aggregations; - const stats: BooleanFieldStats = { - fieldName: field.fieldName, - count: aggregations?.sampled_value_count.doc_count ?? 0, - }; - - const valueBuckets = aggregations?.sampled_values?.buckets ?? []; - valueBuckets.forEach((bucket) => { - stats[`${bucket.key.toString()}Count`] = bucket.doc_count; - }); - return stats; -}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_stats.test.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_stats.test.ts deleted file mode 100644 index 40674a1b5de43..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_stats.test.ts +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; -import { getNumericFieldStatsRequest } from './get_numeric_field_stats'; -import { getKeywordFieldStatsRequest } from './get_keyword_field_stats'; -import { getBooleanFieldStatsRequest } from './get_boolean_field_stats'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { ElasticsearchClient } from '@kbn/core/server'; -import { fetchFieldsStats } from './get_fields_stats'; - -const params = { - index: 'apm-*', - start: 1577836800000, - end: 1609459200000, - includeFrozen: false, - environment: ENVIRONMENT_ALL.value, - kuery: '', -}; - -export const getExpectedQuery = (aggs: any) => { - return { - body: { - aggs, - query: { - bool: { - filter: [ - { term: { 'processor.event': 'transaction' } }, - { - range: { - '@timestamp': { - format: 'epoch_millis', - gte: 1577836800000, - lte: 1609459200000, - }, - }, - }, - ], - }, - }, - }, - index: 'apm-*', - size: 0, - track_total_hits: false, - }; -}; - -describe('field_stats', () => { - describe('getNumericFieldStatsRequest', () => { - it('returns request with filter, percentiles, and top terms aggregations ', () => { - const req = getNumericFieldStatsRequest(params, 'url.path'); - - const expectedAggs = { - sampled_field_stats: { - aggs: { actual_stats: { stats: { field: 'url.path' } } }, - filter: { exists: { field: 'url.path' } }, - }, - sampled_top: { - terms: { - field: 'url.path', - order: { _count: 'desc' }, - size: 10, - }, - }, - }; - expect(req).toEqual(getExpectedQuery(expectedAggs)); - }); - }); - describe('getKeywordFieldStatsRequest', () => { - it('returns request with top terms sampler aggregation ', () => { - const req = getKeywordFieldStatsRequest(params, 'url.path'); - - const expectedAggs = { - sampled_top: { - terms: { field: 'url.path', size: 10 }, - }, - }; - expect(req).toEqual(getExpectedQuery(expectedAggs)); - }); - }); - describe('getBooleanFieldStatsRequest', () => { - it('returns request with top terms sampler aggregation ', () => { - const req = getBooleanFieldStatsRequest(params, 'url.path'); - - const expectedAggs = { - sampled_value_count: { - filter: { exists: { field: 'url.path' } }, - }, - sampled_values: { terms: { field: 'url.path', size: 2 } }, - }; - expect(req).toEqual(getExpectedQuery(expectedAggs)); - }); - }); - - describe('fetchFieldsStats', () => { - it('returns field candidates and total hits', async () => { - const fieldsCaps = { - fields: { - myIpFieldName: { ip: {} }, - myKeywordFieldName: { keyword: {} }, - myMultiFieldName: { keyword: {}, text: {} }, - myHistogramFieldName: { histogram: {} }, - myNumericFieldName: { number: {} }, - }, - }; - const esClientFieldCapsMock = jest.fn(() => fieldsCaps); - - const fieldsToSample = Object.keys(fieldsCaps.fields); - - const esClientSearchMock = jest.fn( - (req: estypes.SearchRequest): estypes.SearchResponse => { - return { - aggregations: { sample: {} }, - } as unknown as estypes.SearchResponse; - } - ); - - const esClientMock = { - fieldCaps: esClientFieldCapsMock, - search: esClientSearchMock, - } as unknown as ElasticsearchClient; - - const resp = await fetchFieldsStats(esClientMock, params, fieldsToSample); - // Should not return stats for unsupported field types like histogram - const expectedFields = [ - 'myIpFieldName', - 'myKeywordFieldName', - 'myMultiFieldName', - 'myNumericFieldName', - ]; - expect(resp.stats.map((s) => s.fieldName)).toEqual(expectedFields); - expect(esClientFieldCapsMock).toHaveBeenCalledTimes(1); - expect(esClientSearchMock).toHaveBeenCalledTimes(4); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_value_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_value_stats.ts deleted file mode 100644 index 6c021b7995dd3..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_value_stats.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ElasticsearchClient } from '@kbn/core/server'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { FieldValuePair } from '../../../../../common/correlations/types'; -import { - FieldStatsCommonRequestParams, - FieldValueFieldStats, - Aggs, - TopValueBucket, -} from '../../../../../common/correlations/field_stats_types'; -import { getQueryWithParams } from '../get_query_with_params'; - -export const getFieldValueFieldStatsRequest = ( - params: FieldStatsCommonRequestParams, - field?: FieldValuePair -): estypes.SearchRequest => { - const query = getQueryWithParams({ params }); - - const { index } = params; - - const size = 0; - const aggs: Aggs = { - filtered_count: { - filter: { - term: { - [`${field?.fieldName}`]: field?.fieldValue, - }, - }, - }, - }; - - const searchBody = { - query, - aggs, - }; - - return { - index, - size, - track_total_hits: false, - body: searchBody, - }; -}; - -export const fetchFieldValueFieldStats = async ( - esClient: ElasticsearchClient, - params: FieldStatsCommonRequestParams, - field: FieldValuePair -): Promise => { - const request = getFieldValueFieldStatsRequest(params, field); - - const body = await esClient.search(request); - const aggregations = body.aggregations as { - filtered_count: estypes.AggregationsSingleBucketAggregateBase; - }; - const topValues: TopValueBucket[] = [ - { - key: field.fieldValue, - doc_count: aggregations.filtered_count.doc_count, - }, - ]; - - const stats = { - fieldName: field.fieldName, - topValues, - topValuesSampleSize: aggregations.filtered_count.doc_count ?? 0, - }; - - return stats; -}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_fields_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_fields_stats.ts deleted file mode 100644 index f0dc541cb5d9d..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_fields_stats.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ElasticsearchClient } from '@kbn/core/server'; -import { chunk } from 'lodash'; -import { ES_FIELD_TYPES } from '@kbn/field-types'; -import { FieldValuePair } from '../../../../../common/correlations/types'; -import { - FieldStats, - FieldStatsCommonRequestParams, -} from '../../../../../common/correlations/field_stats_types'; -import { getRequestBase } from '../get_request_base'; -import { fetchKeywordFieldStats } from './get_keyword_field_stats'; -import { fetchNumericFieldStats } from './get_numeric_field_stats'; -import { fetchBooleanFieldStats } from './get_boolean_field_stats'; - -export const fetchFieldsStats = async ( - esClient: ElasticsearchClient, - fieldStatsParams: FieldStatsCommonRequestParams, - fieldsToSample: string[], - termFilters?: FieldValuePair[] -): Promise<{ stats: FieldStats[]; errors: any[] }> => { - const stats: FieldStats[] = []; - const errors: any[] = []; - - if (fieldsToSample.length === 0) return { stats, errors }; - - const respMapping = await esClient.fieldCaps({ - ...getRequestBase(fieldStatsParams), - fields: fieldsToSample, - }); - - const fieldStatsPromises = Object.entries(respMapping.fields) - .map(([key, value], idx) => { - const field: FieldValuePair = { fieldName: key, fieldValue: '' }; - const fieldTypes = Object.keys(value); - - for (const ft of fieldTypes) { - switch (ft) { - case ES_FIELD_TYPES.KEYWORD: - case ES_FIELD_TYPES.IP: - return fetchKeywordFieldStats( - esClient, - fieldStatsParams, - field, - termFilters - ); - break; - - case 'numeric': - case 'number': - case ES_FIELD_TYPES.FLOAT: - case ES_FIELD_TYPES.HALF_FLOAT: - case ES_FIELD_TYPES.SCALED_FLOAT: - case ES_FIELD_TYPES.DOUBLE: - case ES_FIELD_TYPES.INTEGER: - case ES_FIELD_TYPES.LONG: - case ES_FIELD_TYPES.SHORT: - case ES_FIELD_TYPES.UNSIGNED_LONG: - case ES_FIELD_TYPES.BYTE: - return fetchNumericFieldStats( - esClient, - fieldStatsParams, - field, - termFilters - ); - - break; - case ES_FIELD_TYPES.BOOLEAN: - return fetchBooleanFieldStats( - esClient, - fieldStatsParams, - field, - termFilters - ); - - default: - return; - } - } - }) - .filter((f) => f !== undefined) as Array>; - - const batches = chunk(fieldStatsPromises, 10); - for (let i = 0; i < batches.length; i++) { - try { - const results = await Promise.allSettled(batches[i]); - results.forEach((r) => { - if (r.status === 'fulfilled' && r.value !== undefined) { - stats.push(r.value); - } - }); - } catch (e) { - errors.push(e); - } - } - - return { stats, errors }; -}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_keyword_field_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_keyword_field_stats.ts deleted file mode 100644 index 0661a3f4db373..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_keyword_field_stats.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ElasticsearchClient } from '@kbn/core/server'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { FieldValuePair } from '../../../../../common/correlations/types'; -import { - FieldStatsCommonRequestParams, - KeywordFieldStats, - Aggs, - TopValueBucket, -} from '../../../../../common/correlations/field_stats_types'; -import { getQueryWithParams } from '../get_query_with_params'; - -export const getKeywordFieldStatsRequest = ( - params: FieldStatsCommonRequestParams, - fieldName: string, - termFilters?: FieldValuePair[] -): estypes.SearchRequest => { - const query = getQueryWithParams({ params, termFilters }); - - const { index } = params; - - const size = 0; - const aggs: Aggs = { - sampled_top: { - terms: { - field: fieldName, - size: 10, - }, - }, - }; - - const searchBody = { - query, - aggs, - }; - - return { - index, - size, - track_total_hits: false, - body: searchBody, - }; -}; - -interface SampledTopAggs - extends estypes.AggregationsTermsAggregateBase { - buckets: TopValueBucket[]; -} - -export const fetchKeywordFieldStats = async ( - esClient: ElasticsearchClient, - params: FieldStatsCommonRequestParams, - field: FieldValuePair, - termFilters?: FieldValuePair[] -): Promise => { - const request = getKeywordFieldStatsRequest( - params, - field.fieldName, - termFilters - ); - const body = await esClient.search( - request - ); - const aggregations = body.aggregations; - const topValues = aggregations?.sampled_top?.buckets ?? []; - - const stats = { - fieldName: field.fieldName, - topValues, - topValuesSampleSize: topValues.reduce( - (acc, curr) => acc + curr.doc_count, - aggregations?.sampled_top?.sum_other_doc_count ?? 0 - ), - }; - - return stats; -}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_numeric_field_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_numeric_field_stats.ts deleted file mode 100644 index 085c9975c53d4..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_numeric_field_stats.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ElasticsearchClient } from '@kbn/core/server'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { - NumericFieldStats, - FieldStatsCommonRequestParams, - TopValueBucket, - Aggs, -} from '../../../../../common/correlations/field_stats_types'; -import { FieldValuePair } from '../../../../../common/correlations/types'; -import { getQueryWithParams } from '../get_query_with_params'; - -export const getNumericFieldStatsRequest = ( - params: FieldStatsCommonRequestParams, - fieldName: string, - termFilters?: FieldValuePair[] -) => { - const query = getQueryWithParams({ params, termFilters }); - const size = 0; - - const { index } = params; - - const aggs: Aggs = { - sampled_field_stats: { - filter: { exists: { field: fieldName } }, - aggs: { - actual_stats: { - stats: { field: fieldName }, - }, - }, - }, - sampled_top: { - terms: { - field: fieldName, - size: 10, - order: { - _count: 'desc', - }, - }, - }, - }; - - const searchBody = { - query, - aggs, - }; - - return { - index, - size, - track_total_hits: false, - body: searchBody, - }; -}; - -interface SampledTopAggs - extends estypes.AggregationsTermsAggregateBase { - buckets: TopValueBucket[]; -} -interface StatsAggs { - sampled_top: SampledTopAggs; - sampled_field_stats: { - doc_count: number; - actual_stats: estypes.AggregationsStatsAggregate; - }; -} - -export const fetchNumericFieldStats = async ( - esClient: ElasticsearchClient, - params: FieldStatsCommonRequestParams, - field: FieldValuePair, - termFilters?: FieldValuePair[] -): Promise => { - const request: estypes.SearchRequest = getNumericFieldStatsRequest( - params, - field.fieldName, - termFilters - ); - const body = await esClient.search(request); - - const aggregations = body.aggregations; - const docCount = aggregations?.sampled_field_stats?.doc_count ?? 0; - const fieldStatsResp: Partial = - aggregations?.sampled_field_stats?.actual_stats ?? {}; - const topValues = aggregations?.sampled_top?.buckets ?? []; - - const stats: NumericFieldStats = { - fieldName: field.fieldName, - count: docCount, - min: fieldStatsResp?.min || 0, - max: fieldStatsResp?.max || 0, - avg: fieldStatsResp?.avg || 0, - topValues, - topValuesSampleSize: topValues.reduce( - (acc: number, curr: TopValueBucket) => acc + curr.doc_count, - aggregations?.sampled_top?.sum_other_doc_count ?? 0 - ), - }; - - return stats; -}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/get_common_correlations_query.ts b/x-pack/plugins/apm/server/routes/correlations/queries/get_common_correlations_query.ts new file mode 100644 index 0000000000000..3e70d0ef5765a --- /dev/null +++ b/x-pack/plugins/apm/server/routes/correlations/queries/get_common_correlations_query.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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server'; +import { CommonCorrelationsQueryParams } from '../../../../common/correlations/types'; +import { environmentQuery } from '../../../../common/utils/environment_query'; + +export function getCommonCorrelationsQuery({ + start, + end, + kuery, + query, + environment, +}: CommonCorrelationsQueryParams): QueryDslQueryContainer { + return { + bool: { + filter: [ + query, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ], + }, + }; +} diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/get_filters.ts b/x-pack/plugins/apm/server/routes/correlations/queries/get_filters.ts deleted file mode 100644 index 33f8544baf049..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/get_filters.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ESFilter } from '@kbn/core/types/elasticsearch'; -import { rangeQuery, kqlQuery } from '@kbn/observability-plugin/server'; -import { environmentQuery } from '../../../../common/utils/environment_query'; -import { - SERVICE_NAME, - TRANSACTION_NAME, - TRANSACTION_TYPE, - PROCESSOR_EVENT, -} from '../../../../common/elasticsearch_fieldnames'; -import { ProcessorEvent } from '../../../../common/processor_event'; -import { CorrelationsClientParams } from '../../../../common/correlations/types'; - -export function getCorrelationsFilters({ - environment, - kuery, - serviceName, - transactionType, - transactionName, - start, - end, -}: CorrelationsClientParams) { - const correlationsFilters: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ]; - - if (serviceName) { - correlationsFilters.push({ term: { [SERVICE_NAME]: serviceName } }); - } - - if (transactionType) { - correlationsFilters.push({ term: { [TRANSACTION_TYPE]: transactionType } }); - } - - if (transactionName) { - correlationsFilters.push({ term: { [TRANSACTION_NAME]: transactionName } }); - } - return correlationsFilters; -} diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/get_query_with_params.test.ts b/x-pack/plugins/apm/server/routes/correlations/queries/get_query_with_params.test.ts deleted file mode 100644 index 9d0441e513198..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/get_query_with_params.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; -import { getQueryWithParams } from './get_query_with_params'; - -describe('correlations', () => { - describe('getQueryWithParams', () => { - it('returns the most basic query filtering on processor.event=transaction', () => { - const query = getQueryWithParams({ - params: { - index: 'apm-*', - start: 1577836800000, - end: 1609459200000, - includeFrozen: false, - environment: ENVIRONMENT_ALL.value, - kuery: '', - }, - }); - expect(query).toEqual({ - bool: { - filter: [ - { term: { 'processor.event': 'transaction' } }, - { - range: { - '@timestamp': { - format: 'epoch_millis', - gte: 1577836800000, - lte: 1609459200000, - }, - }, - }, - ], - }, - }); - }); - - it('returns a query considering additional params', () => { - const query = getQueryWithParams({ - params: { - index: 'apm-*', - serviceName: 'actualServiceName', - transactionName: 'actualTransactionName', - start: 1577836800000, - end: 1609459200000, - environment: 'dev', - kuery: '', - includeFrozen: false, - }, - }); - expect(query).toEqual({ - bool: { - filter: [ - { - term: { - 'processor.event': 'transaction', - }, - }, - { - range: { - '@timestamp': { - format: 'epoch_millis', - gte: 1577836800000, - lte: 1609459200000, - }, - }, - }, - { - term: { - 'service.environment': 'dev', - }, - }, - { - term: { - 'service.name': 'actualServiceName', - }, - }, - { - term: { - 'transaction.name': 'actualTransactionName', - }, - }, - ], - }, - }); - }); - - it('returns a query considering a custom field/value pair', () => { - const query = getQueryWithParams({ - params: { - index: 'apm-*', - start: 1577836800000, - end: 1609459200000, - includeFrozen: false, - environment: ENVIRONMENT_ALL.value, - kuery: '', - }, - termFilters: [ - { - fieldName: 'actualFieldName', - fieldValue: 'actualFieldValue', - }, - ], - }); - expect(query).toEqual({ - bool: { - filter: [ - { term: { 'processor.event': 'transaction' } }, - { - range: { - '@timestamp': { - format: 'epoch_millis', - gte: 1577836800000, - lte: 1609459200000, - }, - }, - }, - { - term: { - actualFieldName: 'actualFieldValue', - }, - }, - ], - }, - }); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/get_query_with_params.ts b/x-pack/plugins/apm/server/routes/correlations/queries/get_query_with_params.ts deleted file mode 100644 index 6572d72f614c7..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/get_query_with_params.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { - FieldValuePair, - CorrelationsParams, -} from '../../../../common/correlations/types'; -import { getCorrelationsFilters } from './get_filters'; - -export const getTermsQuery = ({ fieldName, fieldValue }: FieldValuePair) => { - return { term: { [fieldName]: fieldValue } }; -}; - -interface QueryParams { - params: CorrelationsParams; - termFilters?: FieldValuePair[]; -} -export const getQueryWithParams = ({ params, termFilters }: QueryParams) => { - const { - environment, - kuery, - serviceName, - start, - end, - transactionType, - transactionName, - } = params; - - const correlationFilters = getCorrelationsFilters({ - environment, - kuery, - serviceName, - transactionType, - transactionName, - start, - end, - }); - - return { - bool: { - filter: [ - ...correlationFilters, - ...(Array.isArray(termFilters) ? termFilters.map(getTermsQuery) : []), - ] as estypes.QueryDslQueryContainer[], - }, - }; -}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/get_request_base.test.ts b/x-pack/plugins/apm/server/routes/correlations/queries/get_request_base.test.ts deleted file mode 100644 index 9df412b65b8d3..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/get_request_base.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; -import { getRequestBase } from './get_request_base'; - -describe('correlations', () => { - describe('getRequestBase', () => { - it('defaults to not setting `ignore_throttled`', () => { - const requestBase = getRequestBase({ - index: 'apm-*', - environment: ENVIRONMENT_ALL.value, - kuery: '', - start: 1577836800000, - end: 1609459200000, - }); - expect(requestBase.ignore_throttled).toEqual(undefined); - }); - - it('adds `ignore_throttled=false` when `includeFrozen=true`', () => { - const requestBase = getRequestBase({ - index: 'apm-*', - includeFrozen: true, - environment: ENVIRONMENT_ALL.value, - kuery: '', - start: 1577836800000, - end: 1609459200000, - }); - expect(requestBase.ignore_throttled).toEqual(false); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/get_request_base.ts b/x-pack/plugins/apm/server/routes/correlations/queries/get_request_base.ts deleted file mode 100644 index 02719ee3929ce..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/get_request_base.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { CorrelationsParams } from '../../../../common/correlations/types'; - -export const getRequestBase = ({ - index, - includeFrozen, -}: CorrelationsParams) => ({ - index, - // matches APM's event client settings - ...(includeFrozen ? { ignore_throttled: false } : {}), - ignore_unavailable: true, -}); diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/index.ts b/x-pack/plugins/apm/server/routes/correlations/queries/index.ts deleted file mode 100644 index d2a86a20bd5c6..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { fetchFailedTransactionsCorrelationPValues } from './query_failure_correlation'; -export { fetchPValues } from './query_p_values'; -export { fetchSignificantCorrelations } from './query_significant_correlations'; -export { fetchTransactionDurationFieldCandidates } from './query_field_candidates'; -export { fetchTransactionDurationFieldValuePairs } from './query_field_value_pairs'; -export { fetchTransactionDurationFractions } from './query_fractions'; -export { fetchTransactionDurationPercentiles } from './query_percentiles'; -export { fetchTransactionDurationCorrelation } from './query_correlation'; -export { fetchTransactionDurationCorrelationWithHistogram } from './query_correlation_with_histogram'; -export { fetchTransactionDurationHistogramRangeSteps } from './query_histogram_range_steps'; -export { fetchTransactionDurationRanges } from './query_ranges'; -export { fetchFieldValueFieldStats } from './field_stats/get_field_value_stats'; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_correlation.test.ts b/x-pack/plugins/apm/server/routes/correlations/queries/query_correlation.test.ts deleted file mode 100644 index 7573ebaea2ec9..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_correlation.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { ElasticsearchClient } from '@kbn/core/server'; -import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; - -import { - fetchTransactionDurationCorrelation, - getTransactionDurationCorrelationRequest, - BucketCorrelation, -} from './query_correlation'; - -const params = { - index: 'apm-*', - start: 1577836800000, - end: 1609459200000, - includeFrozen: false, - environment: ENVIRONMENT_ALL.value, - kuery: '', -}; -const expectations = [1, 3, 5]; -const ranges = [{ to: 1 }, { from: 1, to: 3 }, { from: 3, to: 5 }, { from: 5 }]; -const fractions = [1, 2, 4, 5]; -const totalDocCount = 1234; - -describe('query_correlation', () => { - describe('getTransactionDurationCorrelationRequest', () => { - it('applies options to the returned query with aggregations for correlations and k-test', () => { - const query = getTransactionDurationCorrelationRequest( - params, - expectations, - ranges, - fractions, - totalDocCount - ); - - expect(query.index).toBe(params.index); - - expect(query?.body?.aggs?.latency_ranges?.range?.field).toBe( - 'transaction.duration.us' - ); - expect(query?.body?.aggs?.latency_ranges?.range?.ranges).toEqual(ranges); - - expect( - ( - query?.body?.aggs?.transaction_duration_correlation as { - bucket_correlation: BucketCorrelation; - } - )?.bucket_correlation.function.count_correlation.indicator - ).toEqual({ - fractions, - expectations, - doc_count: totalDocCount, - }); - - expect( - (query?.body?.aggs?.ks_test as any)?.bucket_count_ks_test?.fractions - ).toEqual(fractions); - }); - }); - - describe('fetchTransactionDurationCorrelation', () => { - it('returns the data from the aggregations', async () => { - const latencyRangesBuckets = [{ to: 1 }, { from: 1, to: 2 }, { from: 2 }]; - const transactionDurationCorrelationValue = 0.45; - const KsTestLess = 0.01; - - const esClientSearchMock = jest.fn( - (req: estypes.SearchRequest): estypes.SearchResponse => { - return { - aggregations: { - latency_ranges: { - buckets: latencyRangesBuckets, - }, - transaction_duration_correlation: { - value: transactionDurationCorrelationValue, - }, - ks_test: { less: KsTestLess }, - }, - } as unknown as estypes.SearchResponse; - } - ); - - const esClientMock = { - search: esClientSearchMock, - } as unknown as ElasticsearchClient; - - const resp = await fetchTransactionDurationCorrelation( - esClientMock, - params, - expectations, - ranges, - fractions, - totalDocCount - ); - - expect(resp).toEqual({ - correlation: transactionDurationCorrelationValue, - ksTest: KsTestLess, - ranges: latencyRangesBuckets, - }); - expect(esClientSearchMock).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_correlation.ts b/x-pack/plugins/apm/server/routes/correlations/queries/query_correlation.ts deleted file mode 100644 index 858d6ea35caff..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_correlation.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { ElasticsearchClient } from '@kbn/core/server'; - -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { - FieldValuePair, - ResponseHit, - CorrelationsParams, -} from '../../../../common/correlations/types'; - -import { getQueryWithParams } from './get_query_with_params'; -import { getRequestBase } from './get_request_base'; - -export interface BucketCorrelation { - buckets_path: string; - function: { - count_correlation: { - indicator: { - fractions: number[]; - expectations: number[]; - doc_count: number; - }; - }; - }; -} - -export const getTransactionDurationCorrelationRequest = ( - params: CorrelationsParams, - expectations: number[], - ranges: estypes.AggregationsAggregationRange[], - fractions: number[], - totalDocCount: number, - termFilters?: FieldValuePair[] -): estypes.SearchRequest => { - const query = getQueryWithParams({ params, termFilters }); - - const bucketCorrelation: BucketCorrelation = { - buckets_path: 'latency_ranges>_count', - function: { - count_correlation: { - indicator: { - fractions, - expectations, - doc_count: totalDocCount, - }, - }, - }, - }; - - const body = { - query, - size: 0, - aggs: { - latency_ranges: { - range: { - field: TRANSACTION_DURATION, - ranges, - }, - }, - // Pearson correlation value - transaction_duration_correlation: { - bucket_correlation: bucketCorrelation, - } as estypes.AggregationsAggregationContainer, - // KS test p value = ks_test.less - ks_test: { - bucket_count_ks_test: { - fractions, - buckets_path: 'latency_ranges>_count', - alternative: ['less', 'greater', 'two_sided'], - }, - } as estypes.AggregationsAggregationContainer, - }, - }; - return { - ...getRequestBase(params), - body, - }; -}; - -interface LatencyRange extends estypes.AggregationsMultiBucketAggregateBase { - buckets: unknown[]; -} -interface Correlation extends estypes.AggregationsRateAggregate { - value: number; -} -interface Aggs { - latency_ranges: LatencyRange; - transaction_duration_correlation: Correlation; - ks_test: { - less: number | null; - }; -} - -export const fetchTransactionDurationCorrelation = async ( - esClient: ElasticsearchClient, - params: CorrelationsParams, - expectations: number[], - ranges: estypes.AggregationsAggregationRange[], - fractions: number[], - totalDocCount: number, - termFilters?: FieldValuePair[] -): Promise<{ - ranges: unknown[]; - correlation: number | null; - ksTest: number | null; -}> => { - const resp = await esClient.search( - getTransactionDurationCorrelationRequest( - params, - expectations, - ranges, - fractions, - totalDocCount, - termFilters - ) - ); - - if (resp.aggregations === undefined) { - throw new Error( - 'fetchTransactionDurationCorrelation failed, did not return aggregations.' - ); - } - - const result = { - ranges: resp.aggregations.latency_ranges.buckets, - correlation: resp.aggregations.transaction_duration_correlation.value, - ksTest: resp.aggregations.ks_test.less, - }; - return result; -}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_correlation_with_histogram.test.ts b/x-pack/plugins/apm/server/routes/correlations/queries/query_correlation_with_histogram.test.ts deleted file mode 100644 index 97e907d1bfd24..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_correlation_with_histogram.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { ElasticsearchClient } from '@kbn/core/server'; -import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; - -import { splitAllSettledPromises } from '../utils'; - -import { fetchTransactionDurationCorrelationWithHistogram } from './query_correlation_with_histogram'; - -const params = { - index: 'apm-*', - start: 1577836800000, - end: 1609459200000, - includeFrozen: false, - environment: ENVIRONMENT_ALL.value, - kuery: '', -}; -const expectations = [1, 3, 5]; -const ranges = [{ to: 1 }, { from: 1, to: 3 }, { from: 3, to: 5 }, { from: 5 }]; -const fractions = [1, 2, 4, 5]; -const totalDocCount = 1234; -const histogramRangeSteps = [1, 2, 4, 5]; - -const fieldValuePairs = [ - { fieldName: 'the-field-name-1', fieldValue: 'the-field-value-1' }, - { fieldName: 'the-field-name-2', fieldValue: 'the-field-value-2' }, - { fieldName: 'the-field-name-2', fieldValue: 'the-field-value-3' }, -]; - -describe('query_correlation_with_histogram', () => { - describe('fetchTransactionDurationCorrelationWithHistogram', () => { - it(`doesn't break on failing ES queries and adds messages to the log`, async () => { - const esClientSearchMock = jest.fn( - (req: estypes.SearchRequest): Promise => { - return Promise.resolve({} as unknown as estypes.SearchResponse); - } - ); - - const esClientMock = { - search: esClientSearchMock, - } as unknown as ElasticsearchClient; - - const { fulfilled: items, rejected: errors } = splitAllSettledPromises( - await Promise.allSettled( - fieldValuePairs.map((fieldValuePair) => - fetchTransactionDurationCorrelationWithHistogram( - esClientMock, - params, - expectations, - ranges, - fractions, - histogramRangeSteps, - totalDocCount, - fieldValuePair - ) - ) - ) - ); - - expect(items.length).toEqual(0); - expect(esClientSearchMock).toHaveBeenCalledTimes(3); - expect(errors.map((e) => (e as Error).toString())).toEqual([ - 'Error: fetchTransactionDurationCorrelation failed, did not return aggregations.', - 'Error: fetchTransactionDurationCorrelation failed, did not return aggregations.', - 'Error: fetchTransactionDurationCorrelation failed, did not return aggregations.', - ]); - }); - - it('returns items with correlation and ks-test value', async () => { - const esClientSearchMock = jest.fn( - (req: estypes.SearchRequest): Promise => { - return Promise.resolve({ - aggregations: { - latency_ranges: { buckets: [] }, - transaction_duration_correlation: { value: 0.6 }, - ks_test: { less: 0.001 }, - logspace_ranges: { buckets: [] }, - }, - } as unknown as estypes.SearchResponse); - } - ); - - const esClientMock = { - search: esClientSearchMock, - } as unknown as ElasticsearchClient; - - const { fulfilled: items, rejected: errors } = splitAllSettledPromises( - await Promise.allSettled( - fieldValuePairs.map((fieldValuePair) => - fetchTransactionDurationCorrelationWithHistogram( - esClientMock, - params, - expectations, - ranges, - fractions, - histogramRangeSteps, - totalDocCount, - fieldValuePair - ) - ) - ) - ); - - expect(items.length).toEqual(3); - expect(esClientSearchMock).toHaveBeenCalledTimes(6); - expect(errors.length).toEqual(0); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_correlation_with_histogram.ts b/x-pack/plugins/apm/server/routes/correlations/queries/query_correlation_with_histogram.ts deleted file mode 100644 index 3410615056670..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_correlation_with_histogram.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { ElasticsearchClient } from '@kbn/core/server'; - -import type { - FieldValuePair, - CorrelationsParams, -} from '../../../../common/correlations/types'; - -import type { LatencyCorrelation } from '../../../../common/correlations/latency_correlations/types'; -import { - CORRELATION_THRESHOLD, - KS_TEST_THRESHOLD, -} from '../../../../common/correlations/constants'; - -import { fetchTransactionDurationCorrelation } from './query_correlation'; -import { fetchTransactionDurationRanges } from './query_ranges'; - -export async function fetchTransactionDurationCorrelationWithHistogram( - esClient: ElasticsearchClient, - params: CorrelationsParams, - expectations: number[], - ranges: estypes.AggregationsAggregationRange[], - fractions: number[], - histogramRangeSteps: number[], - totalDocCount: number, - fieldValuePair: FieldValuePair -) { - const { correlation, ksTest } = await fetchTransactionDurationCorrelation( - esClient, - params, - expectations, - ranges, - fractions, - totalDocCount, - [fieldValuePair] - ); - - if (correlation !== null && ksTest !== null && !isNaN(ksTest)) { - if (correlation > CORRELATION_THRESHOLD && ksTest < KS_TEST_THRESHOLD) { - const logHistogram = await fetchTransactionDurationRanges( - esClient, - params, - histogramRangeSteps, - [fieldValuePair] - ); - return { - ...fieldValuePair, - correlation, - ksTest, - histogram: logHistogram, - } as LatencyCorrelation; - } else { - return { - ...fieldValuePair, - correlation, - ksTest, - } as Omit; - } - } - - return undefined; -} diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_failure_correlation.ts b/x-pack/plugins/apm/server/routes/correlations/queries/query_failure_correlation.ts deleted file mode 100644 index 8e49b6b2dbfe9..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_failure_correlation.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { ElasticsearchClient } from '@kbn/core/server'; -import { CorrelationsParams } from '../../../../common/correlations/types'; -import { FailedTransactionsCorrelation } from '../../../../common/correlations/failed_transactions_correlations/types'; -import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; -import { EventOutcome } from '../../../../common/event_outcome'; -import { fetchTransactionDurationRanges } from './query_ranges'; -import { getQueryWithParams, getTermsQuery } from './get_query_with_params'; -import { getRequestBase } from './get_request_base'; - -export const getFailureCorrelationRequest = ( - params: CorrelationsParams, - fieldName: string -): estypes.SearchRequest => { - const query = getQueryWithParams({ - params, - }); - - const queryWithFailure = { - ...query, - bool: { - ...query.bool, - filter: [ - ...query.bool.filter, - ...[ - getTermsQuery({ - fieldName: EVENT_OUTCOME, - fieldValue: EventOutcome.failure, - }), - ], - ], - }, - }; - - const body = { - query: queryWithFailure, - size: 0, - aggs: { - failure_p_value: { - significant_terms: { - field: fieldName, - background_filter: { - // Important to have same query as above here - // without it, we would be comparing sets of different filtered elements - ...query, - }, - // No need to have must_not "event.outcome": "failure" clause - // if background_is_superset is set to true - p_value: { background_is_superset: true }, - }, - }, - }, - }; - - return { - ...getRequestBase(params), - body, - }; -}; - -interface Aggs extends estypes.AggregationsSignificantLongTermsAggregate { - doc_count: number; - bg_count: number; - buckets: estypes.AggregationsSignificantLongTermsBucket[]; -} - -export const fetchFailedTransactionsCorrelationPValues = async ( - esClient: ElasticsearchClient, - params: CorrelationsParams, - histogramRangeSteps: number[], - fieldName: string -) => { - const resp = await esClient.search( - getFailureCorrelationRequest(params, fieldName) - ); - - if (resp.aggregations === undefined) { - throw new Error( - 'fetchErrorCorrelation failed, did not return aggregations.' - ); - } - - const overallResult = resp.aggregations.failure_p_value; - - // Using for of to sequentially augment the results with histogram data. - const result: FailedTransactionsCorrelation[] = []; - for (const bucket of overallResult.buckets) { - // Scale the score into a value from 0 - 1 - // using a concave piecewise linear function in -log(p-value) - const normalizedScore = - 0.5 * Math.min(Math.max((bucket.score - 3.912) / 2.995, 0), 1) + - 0.25 * Math.min(Math.max((bucket.score - 6.908) / 6.908, 0), 1) + - 0.25 * Math.min(Math.max((bucket.score - 13.816) / 101.314, 0), 1); - - const histogram = await fetchTransactionDurationRanges( - esClient, - params, - histogramRangeSteps, - [{ fieldName, fieldValue: bucket.key }] - ); - - result.push({ - fieldName, - fieldValue: bucket.key, - doc_count: bucket.doc_count, - bg_count: bucket.doc_count, - score: bucket.score, - pValue: Math.exp(-bucket.score), - normalizedScore, - // Percentage of time the term appears in failed transactions - failurePercentage: bucket.doc_count / overallResult.doc_count, - // Percentage of time the term appears in successful transactions - successPercentage: - (bucket.bg_count - bucket.doc_count) / - (overallResult.bg_count - overallResult.doc_count), - histogram, - }); - } - - return result; -}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_field_candidates.test.ts b/x-pack/plugins/apm/server/routes/correlations/queries/query_field_candidates.test.ts deleted file mode 100644 index bbe20474b658d..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_field_candidates.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { ElasticsearchClient } from '@kbn/core/server'; -import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; - -import { hasPrefixToInclude } from '../../../../common/correlations/utils'; - -import { - fetchTransactionDurationFieldCandidates, - getRandomDocsRequest, - shouldBeExcluded, -} from './query_field_candidates'; - -const params = { - index: 'apm-*', - start: 1577836800000, - end: 1609459200000, - includeFrozen: false, - environment: ENVIRONMENT_ALL.value, - kuery: '', -}; - -describe('query_field_candidates', () => { - describe('shouldBeExcluded', () => { - it('does not exclude a completely custom field name', () => { - expect(shouldBeExcluded('myFieldName')).toBe(false); - }); - - it(`excludes a field if it's one of FIELDS_TO_EXCLUDE_AS_CANDIDATE`, () => { - expect(shouldBeExcluded('transaction.type')).toBe(true); - }); - - it(`excludes a field if it's prefixed with one of FIELD_PREFIX_TO_EXCLUDE_AS_CANDIDATE`, () => { - expect(shouldBeExcluded('observer.myFieldName')).toBe(true); - }); - }); - - describe('hasPrefixToInclude', () => { - it('identifies if a field name is prefixed to be included', () => { - expect(hasPrefixToInclude('myFieldName')).toBe(false); - expect(hasPrefixToInclude('somePrefix.myFieldName')).toBe(false); - expect(hasPrefixToInclude('cloud.myFieldName')).toBe(true); - expect(hasPrefixToInclude('labels.myFieldName')).toBe(true); - expect(hasPrefixToInclude('user_agent.myFieldName')).toBe(true); - }); - }); - - describe('getRandomDocsRequest', () => { - it('returns the most basic request body for a sample of random documents', () => { - const req = getRandomDocsRequest(params); - - expect(req).toEqual({ - body: { - _source: false, - fields: ['*'], - query: { - function_score: { - query: { - bool: { - filter: [ - { - term: { - 'processor.event': 'transaction', - }, - }, - { - range: { - '@timestamp': { - format: 'epoch_millis', - gte: 1577836800000, - lte: 1609459200000, - }, - }, - }, - ], - }, - }, - random_score: {}, - }, - }, - size: 1000, - }, - index: params.index, - ignore_throttled: params.includeFrozen ? false : undefined, - ignore_unavailable: true, - }); - }); - }); - - describe('fetchTransactionDurationFieldCandidates', () => { - it('returns field candidates and total hits', async () => { - const esClientFieldCapsMock = jest.fn(() => ({ - fields: { - myIpFieldName: { ip: {} }, - myKeywordFieldName: { keyword: {} }, - myUnpopulatedKeywordFieldName: { keyword: {} }, - myNumericFieldName: { number: {} }, - }, - })); - const esClientSearchMock = jest.fn( - (req: estypes.SearchRequest): estypes.SearchResponse => { - return { - hits: { - hits: [ - { - fields: { - myIpFieldName: '1.1.1.1', - myKeywordFieldName: 'myKeywordFieldValue', - myNumericFieldName: 1234, - }, - }, - ], - }, - } as unknown as estypes.SearchResponse; - } - ); - - const esClientMock = { - fieldCaps: esClientFieldCapsMock, - search: esClientSearchMock, - } as unknown as ElasticsearchClient; - - const resp = await fetchTransactionDurationFieldCandidates( - esClientMock, - params - ); - - expect(resp).toEqual({ - fieldCandidates: [ - // default field candidates - 'service.version', - 'service.node.name', - 'service.framework.version', - 'service.language.version', - 'service.runtime.version', - 'kubernetes.pod.name', - 'kubernetes.pod.uid', - 'container.id', - 'source.ip', - 'client.ip', - 'host.ip', - 'service.environment', - 'process.args', - 'http.response.status_code', - // field candidates identified by sample documents - 'myIpFieldName', - 'myKeywordFieldName', - ], - }); - expect(esClientFieldCapsMock).toHaveBeenCalledTimes(1); - expect(esClientSearchMock).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_field_value_pairs.test.ts b/x-pack/plugins/apm/server/routes/correlations/queries/query_field_value_pairs.test.ts deleted file mode 100644 index 23d5a579bff02..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_field_value_pairs.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { ElasticsearchClient } from '@kbn/core/server'; -import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; - -import { - fetchTransactionDurationFieldValuePairs, - getTermsAggRequest, -} from './query_field_value_pairs'; - -const params = { - index: 'apm-*', - start: 1577836800000, - end: 1609459200000, - includeFrozen: false, - environment: ENVIRONMENT_ALL.value, - kuery: '', -}; - -describe('query_field_value_pairs', () => { - describe('getTermsAggRequest', () => { - it('returns the most basic request body for a terms aggregation', () => { - const fieldName = 'myFieldName'; - const req = getTermsAggRequest(params, fieldName); - expect(req?.body?.aggs?.attribute_terms?.terms?.field).toBe(fieldName); - }); - }); - - describe('fetchTransactionDurationFieldValuePairs', () => { - it('returns field/value pairs for field candidates', async () => { - const fieldCandidates = [ - 'myFieldCandidate1', - 'myFieldCandidate2', - 'myFieldCandidate3', - ]; - - const esClientSearchMock = jest.fn( - (req: estypes.SearchRequest): estypes.SearchResponse => { - return { - aggregations: { - attribute_terms: { - buckets: [{ key: 'myValue1' }, { key: 'myValue2' }], - }, - }, - } as unknown as estypes.SearchResponse; - } - ); - - const esClientMock = { - search: esClientSearchMock, - } as unknown as ElasticsearchClient; - - const resp = await fetchTransactionDurationFieldValuePairs( - esClientMock, - params, - fieldCandidates - ); - - expect(resp.errors).toEqual([]); - expect(resp.fieldValuePairs).toEqual([ - { fieldName: 'myFieldCandidate1', fieldValue: 'myValue1' }, - { fieldName: 'myFieldCandidate1', fieldValue: 'myValue2' }, - { fieldName: 'myFieldCandidate2', fieldValue: 'myValue1' }, - { fieldName: 'myFieldCandidate2', fieldValue: 'myValue2' }, - { fieldName: 'myFieldCandidate3', fieldValue: 'myValue1' }, - { fieldName: 'myFieldCandidate3', fieldValue: 'myValue2' }, - ]); - expect(esClientSearchMock).toHaveBeenCalledTimes(3); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_field_value_pairs.ts b/x-pack/plugins/apm/server/routes/correlations/queries/query_field_value_pairs.ts deleted file mode 100644 index 9546d94ac59ce..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_field_value_pairs.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ElasticsearchClient } from '@kbn/core/server'; - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { - FieldValuePair, - CorrelationsParams, -} from '../../../../common/correlations/types'; -import { TERMS_SIZE } from '../../../../common/correlations/constants'; - -import { splitAllSettledPromises } from '../utils'; - -import { getQueryWithParams } from './get_query_with_params'; -import { getRequestBase } from './get_request_base'; - -export const getTermsAggRequest = ( - params: CorrelationsParams, - fieldName: string -): estypes.SearchRequest => ({ - ...getRequestBase(params), - body: { - query: getQueryWithParams({ params }), - size: 0, - aggs: { - attribute_terms: { - terms: { - field: fieldName, - size: TERMS_SIZE, - }, - }, - }, - }, -}); - -interface Aggs extends estypes.AggregationsMultiBucketAggregateBase { - buckets: Array<{ - key: string; - key_as_string?: string; - }>; -} - -const fetchTransactionDurationFieldTerms = async ( - esClient: ElasticsearchClient, - params: CorrelationsParams, - fieldName: string -): Promise => { - const resp = await esClient.search( - getTermsAggRequest(params, fieldName) - ); - - if (resp.aggregations === undefined) { - throw new Error( - 'fetchTransactionDurationFieldTerms failed, did not return aggregations.' - ); - } - - const buckets = resp.aggregations.attribute_terms?.buckets; - if (buckets?.length >= 1) { - return buckets.map((d) => ({ - fieldName, - // The terms aggregation returns boolean fields as { key: 0, key_as_string: "false" }, - // so we need to pick `key_as_string` if it's present, otherwise searches on boolean fields would fail later on. - fieldValue: d.key_as_string ?? d.key, - })); - } - - return []; -}; - -export const fetchTransactionDurationFieldValuePairs = async ( - esClient: ElasticsearchClient, - params: CorrelationsParams, - fieldCandidates: string[] -): Promise<{ fieldValuePairs: FieldValuePair[]; errors: any[] }> => { - const { fulfilled: responses, rejected: errors } = splitAllSettledPromises( - await Promise.allSettled( - fieldCandidates.map((fieldCandidate) => - fetchTransactionDurationFieldTerms(esClient, params, fieldCandidate) - ) - ) - ); - - return { fieldValuePairs: responses.flat(), errors }; -}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_fractions.test.ts b/x-pack/plugins/apm/server/routes/correlations/queries/query_fractions.test.ts deleted file mode 100644 index 31f2bab8dd738..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_fractions.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { ElasticsearchClient } from '@kbn/core/server'; -import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; - -import { - fetchTransactionDurationFractions, - getTransactionDurationRangesRequest, -} from './query_fractions'; - -const params = { - index: 'apm-*', - start: 1577836800000, - end: 1609459200000, - includeFrozen: false, - environment: ENVIRONMENT_ALL.value, - kuery: '', -}; -const ranges = [{ to: 1 }, { from: 1, to: 3 }, { from: 3, to: 5 }, { from: 5 }]; - -describe('query_fractions', () => { - describe('getTransactionDurationRangesRequest', () => { - it('returns the request body for the transaction duration ranges aggregation', () => { - const req = getTransactionDurationRangesRequest(params, ranges); - - expect(req?.body?.aggs?.latency_ranges?.range?.field).toBe( - 'transaction.duration.us' - ); - expect(req?.body?.aggs?.latency_ranges?.range?.ranges).toEqual(ranges); - }); - }); - - describe('fetchTransactionDurationFractions', () => { - it('computes the actual percentile bucket counts and actual fractions', async () => { - const esClientSearchMock = jest.fn( - (req: estypes.SearchRequest): estypes.SearchResponse => { - return { - hits: { total: { value: 3 } }, - aggregations: { - latency_ranges: { - buckets: [{ doc_count: 1 }, { doc_count: 2 }], - }, - }, - } as unknown as estypes.SearchResponse; - } - ); - - const esClientMock = { - search: esClientSearchMock, - } as unknown as ElasticsearchClient; - - const resp = await fetchTransactionDurationFractions( - esClientMock, - params, - ranges - ); - - expect(resp).toEqual({ - fractions: [0.3333333333333333, 0.6666666666666666], - totalDocCount: 3, - }); - expect(esClientSearchMock).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_fractions.ts b/x-pack/plugins/apm/server/routes/correlations/queries/query_fractions.ts deleted file mode 100644 index c0ec2e7589937..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_fractions.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ElasticsearchClient } from '@kbn/core/server'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import { CorrelationsParams } from '../../../../common/correlations/types'; -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; - -import { getQueryWithParams } from './get_query_with_params'; -import { getRequestBase } from './get_request_base'; - -export const getTransactionDurationRangesRequest = ( - params: CorrelationsParams, - ranges: estypes.AggregationsAggregationRange[] -): estypes.SearchRequest => ({ - ...getRequestBase(params), - body: { - query: getQueryWithParams({ params }), - size: 0, - aggs: { - latency_ranges: { - range: { - field: TRANSACTION_DURATION, - ranges, - }, - }, - }, - }, -}); - -interface Aggs { - latency_ranges: { - buckets: Array<{ - doc_count: number; - }>; - }; -} -/** - * Compute the actual percentile bucket counts and actual fractions - */ -export const fetchTransactionDurationFractions = async ( - esClient: ElasticsearchClient, - params: CorrelationsParams, - ranges: estypes.AggregationsAggregationRange[] -): Promise<{ fractions: number[]; totalDocCount: number }> => { - const resp = await esClient.search( - getTransactionDurationRangesRequest(params, ranges) - ); - - if ((resp.hits.total as estypes.SearchTotalHits).value === 0) { - return { - fractions: [], - totalDocCount: 0, - }; - } - - if (resp.aggregations === undefined) { - throw new Error( - 'fetchTransactionDurationFractions failed, did not return aggregations.' - ); - } - - const buckets = resp.aggregations.latency_ranges?.buckets; - - const totalDocCount = buckets.reduce((acc, bucket) => { - return acc + bucket.doc_count; - }, 0); - - // Compute (doc count per bucket/total doc count) - return { - fractions: buckets.map((bucket) => bucket.doc_count / totalDocCount), - totalDocCount, - }; -}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_histogram.test.ts b/x-pack/plugins/apm/server/routes/correlations/queries/query_histogram.test.ts deleted file mode 100644 index 929f2286d970b..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_histogram.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { ElasticsearchClient } from '@kbn/core/server'; -import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; - -import { - fetchTransactionDurationHistogram, - getTransactionDurationHistogramRequest, -} from './query_histogram'; - -const params = { - index: 'apm-*', - start: 1577836800000, - end: 1609459200000, - includeFrozen: false, - environment: ENVIRONMENT_ALL.value, - kuery: '', -}; -const interval = 100; - -describe('query_histogram', () => { - describe('getTransactionDurationHistogramRequest', () => { - it('returns the request body for the histogram request', () => { - const req = getTransactionDurationHistogramRequest(params, interval); - - expect(req).toEqual({ - body: { - aggs: { - transaction_duration_histogram: { - histogram: { - field: 'transaction.duration.us', - interval, - }, - }, - }, - query: { - bool: { - filter: [ - { - term: { - 'processor.event': 'transaction', - }, - }, - { - range: { - '@timestamp': { - format: 'epoch_millis', - gte: 1577836800000, - lte: 1609459200000, - }, - }, - }, - ], - }, - }, - size: 0, - }, - index: params.index, - ignore_throttled: params.includeFrozen ? false : undefined, - ignore_unavailable: true, - }); - }); - }); - - describe('fetchTransactionDurationHistogram', () => { - it('returns the buckets from the histogram aggregation', async () => { - const histogramBucket = [ - { - key: 0.0, - doc_count: 1, - }, - ]; - - const esClientSearchMock = jest.fn( - (req: estypes.SearchRequest): estypes.SearchResponse => { - return { - aggregations: { - transaction_duration_histogram: { - buckets: histogramBucket, - }, - }, - } as unknown as estypes.SearchResponse; - } - ); - - const esClientMock = { - search: esClientSearchMock, - } as unknown as ElasticsearchClient; - - const resp = await fetchTransactionDurationHistogram( - esClientMock, - params, - interval - ); - - expect(resp).toEqual(histogramBucket); - expect(esClientSearchMock).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_histogram.ts b/x-pack/plugins/apm/server/routes/correlations/queries/query_histogram.ts deleted file mode 100644 index dbf1bd1a6d1f9..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_histogram.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { ElasticsearchClient } from '@kbn/core/server'; - -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { - FieldValuePair, - HistogramItem, - ResponseHit, - CorrelationsParams, -} from '../../../../common/correlations/types'; - -import { getQueryWithParams } from './get_query_with_params'; -import { getRequestBase } from './get_request_base'; - -export const getTransactionDurationHistogramRequest = ( - params: CorrelationsParams, - interval: number, - termFilters?: FieldValuePair[] -): estypes.SearchRequest => ({ - ...getRequestBase(params), - body: { - query: getQueryWithParams({ params, termFilters }), - size: 0, - aggs: { - transaction_duration_histogram: { - histogram: { field: TRANSACTION_DURATION, interval }, - }, - }, - }, -}); - -interface Aggs extends estypes.AggregationsMultiBucketAggregateBase { - buckets: HistogramItem[]; -} - -export const fetchTransactionDurationHistogram = async ( - esClient: ElasticsearchClient, - params: CorrelationsParams, - interval: number, - termFilters?: FieldValuePair[] -): Promise => { - const resp = await esClient.search< - ResponseHit, - { transaction_duration_histogram: Aggs } - >(getTransactionDurationHistogramRequest(params, interval, termFilters)); - - if (resp.aggregations === undefined) { - throw new Error( - 'fetchTransactionDurationHistogram failed, did not return aggregations.' - ); - } - - return resp.aggregations.transaction_duration_histogram.buckets ?? []; -}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_histogram_range_steps.test.ts b/x-pack/plugins/apm/server/routes/correlations/queries/query_histogram_range_steps.test.ts deleted file mode 100644 index 803692f88eb1c..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_histogram_range_steps.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { ElasticsearchClient } from '@kbn/core/server'; -import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; - -import { - fetchTransactionDurationHistogramRangeSteps, - getHistogramIntervalRequest, -} from './query_histogram_range_steps'; - -const params = { - index: 'apm-*', - start: 1577836800000, - end: 1609459200000, - includeFrozen: false, - environment: ENVIRONMENT_ALL.value, - kuery: '', -}; - -describe('query_histogram_range_steps', () => { - describe('getHistogramIntervalRequest', () => { - it('returns the request body for the histogram interval request', () => { - const req = getHistogramIntervalRequest(params); - - expect(req).toEqual({ - body: { - aggs: { - transaction_duration_max: { - max: { - field: 'transaction.duration.us', - }, - }, - transaction_duration_min: { - min: { - field: 'transaction.duration.us', - }, - }, - }, - query: { - bool: { - filter: [ - { - term: { - 'processor.event': 'transaction', - }, - }, - { - range: { - '@timestamp': { - format: 'epoch_millis', - gte: 1577836800000, - lte: 1609459200000, - }, - }, - }, - ], - }, - }, - size: 0, - }, - index: params.index, - ignore_throttled: params.includeFrozen ? false : undefined, - ignore_unavailable: true, - }); - }); - }); - - describe('fetchTransactionDurationHistogramRangeSteps', () => { - it('fetches the range steps for the log histogram', async () => { - const esClientSearchMock = jest.fn( - (req: estypes.SearchRequest): estypes.SearchResponse => { - return { - hits: { total: { value: 10 } }, - aggregations: { - transaction_duration_max: { - value: 10000, - }, - transaction_duration_min: { - value: 10, - }, - }, - } as unknown as estypes.SearchResponse; - } - ); - - const esClientMock = { - search: esClientSearchMock, - } as unknown as ElasticsearchClient; - - const resp = await fetchTransactionDurationHistogramRangeSteps( - esClientMock, - params - ); - - expect(resp.length).toEqual(100); - expect(resp[0]).toEqual(9); - expect(resp[99]).toEqual(18522); - expect(esClientSearchMock).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_histogram_range_steps.ts b/x-pack/plugins/apm/server/routes/correlations/queries/query_histogram_range_steps.ts deleted file mode 100644 index 77b264a2cdb5f..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_histogram_range_steps.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { scaleLog } from 'd3-scale'; - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { ElasticsearchClient } from '@kbn/core/server'; - -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { CorrelationsParams } from '../../../../common/correlations/types'; - -import { getQueryWithParams } from './get_query_with_params'; -import { getRequestBase } from './get_request_base'; - -export const getHistogramRangeSteps = ( - min: number, - max: number, - steps: number -) => { - // A d3 based scale function as a helper to get equally distributed bins on a log scale. - // We round the final values because the ES range agg we use won't accept numbers with decimals for `transaction.duration.us`. - const logFn = scaleLog().domain([min, max]).range([1, steps]); - return [...Array(steps).keys()] - .map(logFn.invert) - .map((d) => (isNaN(d) ? 0 : Math.round(d))); -}; -interface Aggs extends estypes.AggregationsRateAggregate { - value: number; -} - -export const getHistogramIntervalRequest = ( - params: CorrelationsParams -): estypes.SearchRequest => ({ - ...getRequestBase(params), - body: { - query: getQueryWithParams({ params }), - size: 0, - aggs: { - transaction_duration_min: { min: { field: TRANSACTION_DURATION } }, - transaction_duration_max: { max: { field: TRANSACTION_DURATION } }, - }, - }, -}); - -export const fetchTransactionDurationHistogramRangeSteps = async ( - esClient: ElasticsearchClient, - params: CorrelationsParams -): Promise => { - const steps = 100; - - const resp = await esClient.search< - unknown, - { - transaction_duration_min: Aggs; - transaction_duration_max: Aggs; - } - >(getHistogramIntervalRequest(params)); - - if ((resp.hits.total as estypes.SearchTotalHits).value === 0) { - return getHistogramRangeSteps(0, 1, 100); - } - - if (resp.aggregations === undefined) { - throw new Error( - 'fetchTransactionDurationHistogramRangeSteps failed, did not return aggregations.' - ); - } - - const min = resp.aggregations.transaction_duration_min.value; - const max = resp.aggregations.transaction_duration_max.value * 2; - - return getHistogramRangeSteps(min, max, steps); -}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_percentiles.test.ts b/x-pack/plugins/apm/server/routes/correlations/queries/query_percentiles.test.ts deleted file mode 100644 index 9904df5d068e8..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_percentiles.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { ElasticsearchClient } from '@kbn/core/server'; -import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; - -import { - fetchTransactionDurationPercentiles, - getTransactionDurationPercentilesRequest, -} from './query_percentiles'; - -const params = { - index: 'apm-*', - start: 1577836800000, - end: 1609459200000, - includeFrozen: false, - environment: ENVIRONMENT_ALL.value, - kuery: '', -}; - -describe('query_percentiles', () => { - describe('getTransactionDurationPercentilesRequest', () => { - it('returns the request body for the duration percentiles request', () => { - const req = getTransactionDurationPercentilesRequest(params); - - expect(req).toEqual({ - body: { - aggs: { - transaction_duration_percentiles: { - percentiles: { - field: 'transaction.duration.us', - hdr: { - number_of_significant_value_digits: 3, - }, - }, - }, - }, - query: { - bool: { - filter: [ - { - term: { - 'processor.event': 'transaction', - }, - }, - { - range: { - '@timestamp': { - format: 'epoch_millis', - gte: 1577836800000, - lte: 1609459200000, - }, - }, - }, - ], - }, - }, - size: 0, - track_total_hits: true, - }, - index: params.index, - ignore_throttled: params.includeFrozen ? false : undefined, - ignore_unavailable: true, - }); - }); - }); - - describe('fetchTransactionDurationPercentiles', () => { - it('fetches the percentiles', async () => { - const totalDocs = 10; - const percentilesValues = { - '1.0': 5.0, - '5.0': 25.0, - '25.0': 165.0, - '50.0': 445.0, - '75.0': 725.0, - '95.0': 945.0, - '99.0': 985.0, - }; - - const esClientSearchMock = jest.fn( - (req: estypes.SearchRequest): estypes.SearchResponse => { - return { - hits: { total: { value: totalDocs } }, - aggregations: { - transaction_duration_percentiles: { - values: percentilesValues, - }, - }, - } as unknown as estypes.SearchResponse; - } - ); - const esClientMock = { - search: esClientSearchMock, - } as unknown as ElasticsearchClient; - - const resp = await fetchTransactionDurationPercentiles( - esClientMock, - params - ); - - expect(resp).toEqual({ percentiles: percentilesValues, totalDocs }); - expect(esClientSearchMock).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_percentiles.ts b/x-pack/plugins/apm/server/routes/correlations/queries/query_percentiles.ts deleted file mode 100644 index fba4e68450b07..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_percentiles.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { ElasticsearchClient } from '@kbn/core/server'; - -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import { SIGNIFICANT_VALUE_DIGITS } from '../../../../common/correlations/constants'; -import type { - FieldValuePair, - ResponseHit, - CorrelationsParams, -} from '../../../../common/correlations/types'; - -import { getQueryWithParams } from './get_query_with_params'; -import { getRequestBase } from './get_request_base'; - -export const getTransactionDurationPercentilesRequest = ( - params: CorrelationsParams, - percents?: number[], - termFilters?: FieldValuePair[] -): estypes.SearchRequest => { - const query = getQueryWithParams({ params, termFilters }); - - return { - ...getRequestBase(params), - body: { - track_total_hits: true, - query, - size: 0, - aggs: { - transaction_duration_percentiles: { - percentiles: { - hdr: { - number_of_significant_value_digits: SIGNIFICANT_VALUE_DIGITS, - }, - field: TRANSACTION_DURATION, - ...(Array.isArray(percents) ? { percents } : {}), - }, - }, - }, - }, - }; -}; - -interface Aggs extends estypes.AggregationsTDigestPercentilesAggregate { - values: Record; -} - -export const fetchTransactionDurationPercentiles = async ( - esClient: ElasticsearchClient, - params: CorrelationsParams, - percents?: number[], - termFilters?: FieldValuePair[] -): Promise<{ totalDocs: number; percentiles: Record }> => { - const resp = await esClient.search< - ResponseHit, - { transaction_duration_percentiles: Aggs } - >(getTransactionDurationPercentilesRequest(params, percents, termFilters)); - - // return early with no results if the search didn't return any documents - if ((resp.hits.total as estypes.SearchTotalHits).value === 0) { - return { totalDocs: 0, percentiles: {} }; - } - - if (resp.aggregations === undefined) { - throw new Error( - 'fetchTransactionDurationPercentiles failed, did not return aggregations.' - ); - } - - return { - totalDocs: (resp.hits.total as estypes.SearchTotalHits).value, - percentiles: - resp.aggregations.transaction_duration_percentiles.values ?? {}, - }; -}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_ranges.test.ts b/x-pack/plugins/apm/server/routes/correlations/queries/query_ranges.test.ts deleted file mode 100644 index d4a0e3fab87bb..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_ranges.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { ElasticsearchClient } from '@kbn/core/server'; -import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; - -import { - fetchTransactionDurationRanges, - getTransactionDurationRangesRequest, -} from './query_ranges'; - -const params = { - index: 'apm-*', - start: 1577836800000, - end: 1609459200000, - includeFrozen: false, - environment: ENVIRONMENT_ALL.value, - kuery: '', -}; -const rangeSteps = [1, 3, 5]; - -describe('query_ranges', () => { - describe('getTransactionDurationRangesRequest', () => { - it('returns the request body for the duration percentiles request', () => { - const req = getTransactionDurationRangesRequest(params, rangeSteps); - - expect(req).toEqual({ - body: { - aggs: { - logspace_ranges: { - range: { - field: 'transaction.duration.us', - ranges: [ - { - to: 0, - }, - { - from: 0, - to: 1, - }, - { - from: 1, - to: 3, - }, - { - from: 3, - to: 5, - }, - { - from: 5, - }, - ], - }, - }, - }, - query: { - bool: { - filter: [ - { - term: { - 'processor.event': 'transaction', - }, - }, - { - range: { - '@timestamp': { - format: 'epoch_millis', - gte: 1577836800000, - lte: 1609459200000, - }, - }, - }, - ], - }, - }, - size: 0, - }, - index: params.index, - ignore_throttled: params.includeFrozen ? false : undefined, - ignore_unavailable: true, - }); - }); - }); - - describe('fetchTransactionDurationRanges', () => { - it('fetches the percentiles', async () => { - const logspaceRangesBuckets = [ - { - key: '*-100.0', - to: 100.0, - doc_count: 2, - }, - { - key: '100.0-200.0', - from: 100.0, - to: 200.0, - doc_count: 2, - }, - { - key: '200.0-*', - from: 200.0, - doc_count: 3, - }, - ]; - - const esClientSearchMock = jest.fn( - (req: estypes.SearchRequest): estypes.SearchResponse => { - return { - aggregations: { - logspace_ranges: { - buckets: logspaceRangesBuckets, - }, - }, - } as unknown as estypes.SearchResponse; - } - ); - - const esClientMock = { - search: esClientSearchMock, - } as unknown as ElasticsearchClient; - - const resp = await fetchTransactionDurationRanges( - esClientMock, - params, - rangeSteps - ); - - expect(resp).toEqual([ - { doc_count: 2, key: 100 }, - { doc_count: 3, key: 200 }, - ]); - expect(esClientSearchMock).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/query_ranges.ts b/x-pack/plugins/apm/server/routes/correlations/queries/query_ranges.ts deleted file mode 100644 index 6f56ca6b9ff8f..0000000000000 --- a/x-pack/plugins/apm/server/routes/correlations/queries/query_ranges.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import type { ElasticsearchClient } from '@kbn/core/server'; - -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { - FieldValuePair, - ResponseHit, - CorrelationsParams, -} from '../../../../common/correlations/types'; - -import { getQueryWithParams } from './get_query_with_params'; -import { getRequestBase } from './get_request_base'; - -export const getTransactionDurationRangesRequest = ( - params: CorrelationsParams, - rangesSteps: number[], - termFilters?: FieldValuePair[] -): estypes.SearchRequest => { - const query = getQueryWithParams({ params, termFilters }); - - const ranges = rangesSteps.reduce( - (p, to) => { - const from = p[p.length - 1].to; - p.push({ from, to }); - return p; - }, - [{ to: 0 }] as Array<{ from?: number; to?: number }> - ); - if (ranges.length > 0) { - ranges.push({ from: ranges[ranges.length - 1].to }); - } - - return { - ...getRequestBase(params), - body: { - query, - size: 0, - aggs: { - logspace_ranges: { - range: { - field: TRANSACTION_DURATION, - ranges, - }, - }, - }, - }, - }; -}; - -interface Aggs extends estypes.AggregationsMultiBucketAggregateBase { - buckets: Array<{ - from: number; - doc_count: number; - }>; -} - -export const fetchTransactionDurationRanges = async ( - esClient: ElasticsearchClient, - params: CorrelationsParams, - rangesSteps: number[], - termFilters?: FieldValuePair[] -): Promise> => { - const resp = await esClient.search( - getTransactionDurationRangesRequest(params, rangesSteps, termFilters) - ); - - if (resp.aggregations === undefined) { - throw new Error( - 'fetchTransactionDurationCorrelation failed, did not return aggregations.' - ); - } - - return resp.aggregations.logspace_ranges.buckets - .map((d) => ({ - key: d.from, - doc_count: d.doc_count, - })) - .filter((d) => d.key !== undefined); -}; diff --git a/x-pack/plugins/apm/server/routes/correlations/route.ts b/x-pack/plugins/apm/server/routes/correlations/route.ts index b2bca48574f5d..7d5e9db935e45 100644 --- a/x-pack/plugins/apm/server/routes/correlations/route.ts +++ b/x-pack/plugins/apm/server/routes/correlations/route.ts @@ -11,37 +11,33 @@ import Boom from '@hapi/boom'; import { i18n } from '@kbn/i18n'; import { toNumberRt } from '@kbn/io-ts-utils'; +import { termQuery } from '@kbn/observability-plugin/server'; import { isActivePlatinumLicense } from '../../../common/license_check'; import { setupRequest } from '../../lib/helpers/setup_request'; -import { - fetchPValues, - fetchSignificantCorrelations, - fetchTransactionDurationFieldCandidates, - fetchTransactionDurationFieldValuePairs, - fetchFieldValueFieldStats, -} from './queries'; -import { fetchFieldsStats } from './queries/field_stats/get_fields_stats'; - -import { withApmSpan } from '../../utils/with_apm_span'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { environmentRt, kueryRt, rangeRt } from '../default_api_types'; -import { LatencyCorrelation } from '../../../common/correlations/latency_correlations/types'; +import { fetchDurationFieldCandidates } from './queries/fetch_duration_field_candidates'; +import { ProcessorEvent } from '../../../common/processor_event'; import { - FieldStats, - TopValuesStats, -} from '../../../common/correlations/field_stats_types'; -import { FieldValuePair } from '../../../common/correlations/types'; -import { FailedTransactionsCorrelation } from '../../../common/correlations/failed_transactions_correlations/types'; + SERVICE_NAME, + TRANSACTION_NAME, + TRANSACTION_TYPE, +} from '../../../common/elasticsearch_fieldnames'; +import { fetchFieldValueFieldStats } from './queries/field_stats/fetch_field_value_field_stats'; +import { fetchFieldValuePairs } from './queries/fetch_field_value_pairs'; +import { fetchSignificantCorrelations } from './queries/fetch_significant_correlations'; +import { fetchFieldsStats } from './queries/field_stats/fetch_fields_stats'; +import { fetchPValues } from './queries/fetch_p_values'; const INVALID_LICENSE = i18n.translate('xpack.apm.correlations.license.text', { defaultMessage: 'To use the correlations API, you must be subscribed to an Elastic Platinum license.', }); -const fieldCandidatesRoute = createApmServerRoute({ - endpoint: 'GET /internal/apm/correlations/field_candidates', +const fieldCandidatesTransactionsRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/correlations/field_candidates/transactions', params: t.type({ query: t.intersection([ t.partial({ @@ -62,23 +58,42 @@ const fieldCandidatesRoute = createApmServerRoute({ throw Boom.forbidden(INVALID_LICENSE); } - const { indices } = await setupRequest(resources); - const esClient = (await resources.context.core).elasticsearch.client - .asCurrentUser; - - return withApmSpan( - 'get_correlations_field_candidates', - async (): Promise<{ fieldCandidates: string[] }> => - await fetchTransactionDurationFieldCandidates(esClient, { - ...resources.params.query, - index: indices.transaction, - }) - ); + const setup = await setupRequest(resources); + + const { + query: { + serviceName, + transactionName, + transactionType, + start, + end, + environment, + kuery, + }, + } = resources.params; + + return fetchDurationFieldCandidates({ + eventType: ProcessorEvent.transaction, + start, + end, + environment, + kuery, + query: { + bool: { + filter: [ + ...termQuery(SERVICE_NAME, serviceName), + ...termQuery(TRANSACTION_TYPE, transactionType), + ...termQuery(TRANSACTION_NAME, transactionName), + ], + }, + }, + setup, + }); }, }); -const fieldStatsRoute = createApmServerRoute({ - endpoint: 'POST /internal/apm/correlations/field_stats', +const fieldStatsTransactionsRoute = createApmServerRoute({ + endpoint: 'POST /internal/apm/correlations/field_stats/transactions', params: t.type({ body: t.intersection([ t.partial({ @@ -109,29 +124,44 @@ const fieldStatsRoute = createApmServerRoute({ throw Boom.forbidden(INVALID_LICENSE); } - const { indices } = await setupRequest(resources); - const esClient = (await resources.context.core).elasticsearch.client - .asCurrentUser; - - const { fieldsToSample, ...params } = resources.params.body; - - return withApmSpan( - 'get_correlations_field_stats', - async (): Promise<{ stats: FieldStats[]; errors: any[] }> => - await fetchFieldsStats( - esClient, - { - ...params, - index: indices.transaction, - }, - fieldsToSample - ) - ); + const setup = await setupRequest(resources); + + const { + body: { + serviceName, + transactionName, + transactionType, + start, + end, + environment, + kuery, + fieldsToSample, + }, + } = resources.params; + + return fetchFieldsStats({ + setup, + eventType: ProcessorEvent.transaction, + start, + end, + environment, + kuery, + query: { + bool: { + filter: [ + ...termQuery(SERVICE_NAME, serviceName), + ...termQuery(TRANSACTION_TYPE, transactionType), + ...termQuery(TRANSACTION_NAME, transactionName), + ], + }, + }, + fieldsToSample, + }); }, }); -const fieldValueStatsRoute = createApmServerRoute({ - endpoint: 'GET /internal/apm/correlations/field_value_stats', +const fieldValueStatsTransactionsRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/correlations/field_value_stats/transactions', params: t.type({ query: t.intersection([ t.partial({ @@ -160,29 +190,48 @@ const fieldValueStatsRoute = createApmServerRoute({ throw Boom.forbidden(INVALID_LICENSE); } - const { indices } = await setupRequest(resources); - const esClient = (await resources.context.core).elasticsearch.client - .asCurrentUser; - - const { fieldName, fieldValue, ...params } = resources.params.query; - - return withApmSpan( - 'get_correlations_field_value_stats', - async (): Promise => - await fetchFieldValueFieldStats( - esClient, - { - ...params, - index: indices.transaction, - }, - { fieldName, fieldValue } - ) - ); + const setup = await setupRequest(resources); + + const { + query: { + serviceName, + transactionName, + transactionType, + start, + end, + environment, + kuery, + fieldName, + fieldValue, + }, + } = resources.params; + + return fetchFieldValueFieldStats({ + setup, + eventType: ProcessorEvent.transaction, + start, + end, + environment, + kuery, + query: { + bool: { + filter: [ + ...termQuery(SERVICE_NAME, serviceName), + ...termQuery(TRANSACTION_TYPE, transactionType), + ...termQuery(TRANSACTION_NAME, transactionName), + ], + }, + }, + field: { + fieldName, + fieldValue, + }, + }); }, }); -const fieldValuePairsRoute = createApmServerRoute({ - endpoint: 'POST /internal/apm/correlations/field_value_pairs', +const fieldValuePairsTransactionsRoute = createApmServerRoute({ + endpoint: 'POST /internal/apm/correlations/field_value_pairs/transactions', params: t.type({ body: t.intersection([ t.partial({ @@ -213,29 +262,45 @@ const fieldValuePairsRoute = createApmServerRoute({ throw Boom.forbidden(INVALID_LICENSE); } - const { indices } = await setupRequest(resources); - const esClient = (await resources.context.core).elasticsearch.client - .asCurrentUser; - - const { fieldCandidates, ...params } = resources.params.body; - - return withApmSpan( - 'get_correlations_field_value_pairs', - async (): Promise<{ errors: any[]; fieldValuePairs: FieldValuePair[] }> => - await fetchTransactionDurationFieldValuePairs( - esClient, - { - ...params, - index: indices.transaction, - }, - fieldCandidates - ) - ); + const setup = await setupRequest(resources); + + const { + body: { + serviceName, + transactionName, + transactionType, + start, + end, + environment, + kuery, + fieldCandidates, + }, + } = resources.params; + + return fetchFieldValuePairs({ + setup, + eventType: ProcessorEvent.transaction, + start, + end, + environment, + kuery, + query: { + bool: { + filter: [ + ...termQuery(SERVICE_NAME, serviceName), + ...termQuery(TRANSACTION_TYPE, transactionType), + ...termQuery(TRANSACTION_NAME, transactionName), + ], + }, + }, + fieldCandidates, + }); }, }); -const significantCorrelationsRoute = createApmServerRoute({ - endpoint: 'POST /internal/apm/correlations/significant_correlations', +const significantCorrelationsTransactionsRoute = createApmServerRoute({ + endpoint: + 'POST /internal/apm/correlations/significant_correlations/transactions', params: t.type({ body: t.intersection([ t.partial({ @@ -267,41 +332,44 @@ const significantCorrelationsRoute = createApmServerRoute({ totalDocCount: number; fallbackResult?: import('./../../../common/correlations/latency_correlations/types').LatencyCorrelation; }> => { - const { context } = resources; - const { license } = await context.licensing; - if (!isActivePlatinumLicense(license)) { - throw Boom.forbidden(INVALID_LICENSE); - } - - const { indices } = await setupRequest(resources); - const esClient = (await resources.context.core).elasticsearch.client - .asCurrentUser; - - const { fieldValuePairs, ...params } = resources.params.body; - - const paramsWithIndex = { - ...params, - index: indices.transaction, - }; - - return withApmSpan( - 'get_significant_correlations', - async (): Promise<{ - latencyCorrelations: LatencyCorrelation[]; - ccsWarning: boolean; - totalDocCount: number; - }> => - await fetchSignificantCorrelations( - esClient, - paramsWithIndex, - fieldValuePairs - ) - ); + const setup = await setupRequest(resources); + + const { + body: { + serviceName, + transactionName, + transactionType, + start, + end, + environment, + kuery, + fieldValuePairs, + }, + } = resources.params; + + return fetchSignificantCorrelations({ + setup, + eventType: ProcessorEvent.transaction, + start, + end, + environment, + kuery, + query: { + bool: { + filter: [ + ...termQuery(SERVICE_NAME, serviceName), + ...termQuery(TRANSACTION_TYPE, transactionType), + ...termQuery(TRANSACTION_NAME, transactionName), + ], + }, + }, + fieldValuePairs, + }); }, }); -const pValuesRoute = createApmServerRoute({ - endpoint: 'POST /internal/apm/correlations/p_values', +const pValuesTransactionsRoute = createApmServerRoute({ + endpoint: 'POST /internal/apm/correlations/p_values/transactions', params: t.type({ body: t.intersection([ t.partial({ @@ -327,38 +395,47 @@ const pValuesRoute = createApmServerRoute({ ccsWarning: boolean; fallbackResult?: import('./../../../common/correlations/failed_transactions_correlations/types').FailedTransactionsCorrelation; }> => { - const { context } = resources; - const { license } = await context.licensing; - if (!isActivePlatinumLicense(license)) { - throw Boom.forbidden(INVALID_LICENSE); - } - - const { indices } = await setupRequest(resources); - const esClient = (await resources.context.core).elasticsearch.client - .asCurrentUser; - - const { fieldCandidates, ...params } = resources.params.body; - - const paramsWithIndex = { - ...params, - index: indices.transaction, - }; - - return withApmSpan( - 'get_p_values', - async (): Promise<{ - failedTransactionsCorrelations: FailedTransactionsCorrelation[]; - ccsWarning: boolean; - }> => await fetchPValues(esClient, paramsWithIndex, fieldCandidates) - ); + const setup = await setupRequest(resources); + + const { + body: { + serviceName, + transactionName, + transactionType, + start, + end, + environment, + kuery, + fieldCandidates, + }, + } = resources.params; + + return fetchPValues({ + setup, + eventType: ProcessorEvent.transaction, + start, + end, + environment, + kuery, + query: { + bool: { + filter: [ + ...termQuery(SERVICE_NAME, serviceName), + ...termQuery(TRANSACTION_TYPE, transactionType), + ...termQuery(TRANSACTION_NAME, transactionName), + ], + }, + }, + fieldCandidates, + }); }, }); export const correlationsRouteRepository = { - ...pValuesRoute, - ...fieldCandidatesRoute, - ...fieldStatsRoute, - ...fieldValueStatsRoute, - ...fieldValuePairsRoute, - ...significantCorrelationsRoute, + ...fieldCandidatesTransactionsRoute, + ...fieldStatsTransactionsRoute, + ...fieldValueStatsTransactionsRoute, + ...fieldValuePairsTransactionsRoute, + ...significantCorrelationsTransactionsRoute, + ...pValuesTransactionsRoute, }; diff --git a/x-pack/plugins/apm/server/routes/latency_distribution/get_overall_latency_distribution.ts b/x-pack/plugins/apm/server/routes/latency_distribution/get_overall_latency_distribution.ts index d8e4cf7af0bc5..599e130c16864 100644 --- a/x-pack/plugins/apm/server/routes/latency_distribution/get_overall_latency_distribution.ts +++ b/x-pack/plugins/apm/server/routes/latency_distribution/get_overall_latency_distribution.ts @@ -6,47 +6,53 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { Environment } from '../../../common/environment_rt'; import { ProcessorEvent } from '../../../common/processor_event'; +import { Setup } from '../../lib/helpers/setup_request'; import { withApmSpan } from '../../utils/with_apm_span'; -import { - getHistogramIntervalRequest, - getHistogramRangeSteps, -} from '../correlations/queries/query_histogram_range_steps'; -import { getTransactionDurationRangesRequest } from '../correlations/queries/query_ranges'; +import { fetchDurationRanges } from '../correlations/queries/fetch_duration_ranges'; +import { fetchDurationHistogramRangeSteps } from '../correlations/queries/fetch_duration_histogram_range_steps'; import { getPercentileThresholdValue } from './get_percentile_threshold_value'; -import type { - OverallLatencyDistributionOptions, - OverallLatencyDistributionResponse, -} from './types'; - -interface Aggs extends estypes.AggregationsMultiBucketAggregateBase { - buckets: Array<{ - from: number; - doc_count: number; - }>; -} -export async function getOverallLatencyDistribution( - options: OverallLatencyDistributionOptions -) { +import type { OverallLatencyDistributionResponse } from './types'; + +export async function getOverallLatencyDistribution({ + eventType, + setup, + start, + end, + environment, + kuery, + query, + percentileThreshold, +}: { + eventType: ProcessorEvent; + setup: Setup; + start: number; + end: number; + environment: Environment; + kuery: string; + query: estypes.QueryDslQueryContainer; + percentileThreshold: number; +}) { return withApmSpan('get_overall_latency_distribution', async () => { const overallLatencyDistribution: OverallLatencyDistributionResponse = {}; - const { setup, termFilters, ...rawParams } = options; - const { apmEventClient } = setup; - const params = { - // pass on an empty index because we're using only the body attribute - // of the request body getters we're reusing from search strategies. - index: '', - ...rawParams, - }; - // #1: get 95th percentile to be displayed as a marker in the log log chart overallLatencyDistribution.percentileThresholdValue = - await getPercentileThresholdValue(options); + await getPercentileThresholdValue({ + eventType, + setup, + start, + end, + environment, + kuery, + query, + percentileThreshold, + }); // finish early if we weren't able to identify the percentileThresholdValue. if (!overallLatencyDistribution.percentileThresholdValue) { @@ -54,72 +60,34 @@ export async function getOverallLatencyDistribution( } // #2: get histogram range steps - const steps = 100; - - const { body: histogramIntervalRequestBody } = - getHistogramIntervalRequest(params); - - const histogramIntervalResponse = (await apmEventClient.search( - 'get_histogram_interval', - { - // TODO: add support for metrics - apm: { events: [ProcessorEvent.transaction] }, - body: { size: 0, ...histogramIntervalRequestBody }, - } - )) as { - aggregations?: { - transaction_duration_min: estypes.AggregationsRateAggregate; - transaction_duration_max: estypes.AggregationsRateAggregate; - }; - hits: { total: estypes.SearchTotalHits }; - }; - - if ( - !histogramIntervalResponse.aggregations || - histogramIntervalResponse.hits.total.value === 0 - ) { + const rangeSteps = await fetchDurationHistogramRangeSteps({ + eventType, + setup, + start, + end, + environment, + kuery, + query, + }); + + if (!rangeSteps) { return overallLatencyDistribution; } - const min = - histogramIntervalResponse.aggregations.transaction_duration_min.value; - const max = - histogramIntervalResponse.aggregations.transaction_duration_max.value * 2; - - const histogramRangeSteps = getHistogramRangeSteps(min, max, steps); - // #3: get histogram chart data - const { body: transactionDurationRangesRequestBody } = - getTransactionDurationRangesRequest( - params, - histogramRangeSteps, - termFilters - ); - - const transactionDurationRangesResponse = (await apmEventClient.search( - 'get_transaction_duration_ranges', - { - // TODO: add support for metrics - apm: { events: [ProcessorEvent.transaction] }, - body: { size: 0, ...transactionDurationRangesRequestBody }, - } - )) as { - aggregations?: { - logspace_ranges: Aggs; - }; - }; - - if (!transactionDurationRangesResponse.aggregations) { - return overallLatencyDistribution; - } - overallLatencyDistribution.overallHistogram = - transactionDurationRangesResponse.aggregations.logspace_ranges.buckets - .map((d) => ({ - key: d.from, - doc_count: d.doc_count, - })) - .filter((d) => d.key !== undefined); + const durationRanges = await fetchDurationRanges({ + eventType, + setup, + start, + end, + environment, + kuery, + query, + rangeSteps, + }); + + overallLatencyDistribution.overallHistogram = durationRanges; return overallLatencyDistribution; }); diff --git a/x-pack/plugins/apm/server/routes/latency_distribution/get_percentile_threshold_value.ts b/x-pack/plugins/apm/server/routes/latency_distribution/get_percentile_threshold_value.ts index c40834919f7f5..301c14aeded4c 100644 --- a/x-pack/plugins/apm/server/routes/latency_distribution/get_percentile_threshold_value.ts +++ b/x-pack/plugins/apm/server/routes/latency_distribution/get_percentile_threshold_value.ts @@ -5,49 +5,34 @@ * 2.0. */ +import { CommonCorrelationsQueryParams } from '../../../common/correlations/types'; import { ProcessorEvent } from '../../../common/processor_event'; - -import { getTransactionDurationPercentilesRequest } from '../correlations/queries/query_percentiles'; - -import type { OverallLatencyDistributionOptions } from './types'; - -export async function getPercentileThresholdValue( - options: OverallLatencyDistributionOptions -) { - const { setup, percentileThreshold, ...rawParams } = options; - const { apmEventClient } = setup; - const params = { - // pass on an empty index because we're using only the body attribute - // of the request body getters we're reusing from search strategies. - index: '', - ...rawParams, - }; - - const { body: transactionDurationPercentilesRequestBody } = - getTransactionDurationPercentilesRequest(params, [percentileThreshold]); - - const transactionDurationPercentilesResponse = (await apmEventClient.search( - 'get_transaction_duration_percentiles', - { - // TODO: add support for metrics - apm: { events: [ProcessorEvent.transaction] }, - body: { size: 0, ...transactionDurationPercentilesRequestBody }, - } - )) as { - aggregations?: { - transaction_duration_percentiles: { - values: Record; - }; - }; - }; - - if (!transactionDurationPercentilesResponse.aggregations) { - return; - } - - const percentilesResponseThresholds = - transactionDurationPercentilesResponse.aggregations - .transaction_duration_percentiles?.values ?? {}; - - return percentilesResponseThresholds[`${percentileThreshold}.0`]; +import { Setup } from '../../lib/helpers/setup_request'; +import { fetchDurationPercentiles } from '../correlations/queries/fetch_duration_percentiles'; + +export async function getPercentileThresholdValue({ + setup, + eventType, + start, + end, + environment, + kuery, + query, + percentileThreshold, +}: CommonCorrelationsQueryParams & { + setup: Setup; + eventType: ProcessorEvent; + percentileThreshold: number; +}) { + const durationPercentiles = await fetchDurationPercentiles({ + setup, + eventType, + start, + end, + environment, + kuery, + query, + }); + + return durationPercentiles.percentiles[`${percentileThreshold}.0`]; } diff --git a/x-pack/plugins/apm/server/routes/latency_distribution/route.ts b/x-pack/plugins/apm/server/routes/latency_distribution/route.ts index a51c03238709e..9ab93e11b974b 100644 --- a/x-pack/plugins/apm/server/routes/latency_distribution/route.ts +++ b/x-pack/plugins/apm/server/routes/latency_distribution/route.ts @@ -7,13 +7,21 @@ import * as t from 'io-ts'; import { toNumberRt } from '@kbn/io-ts-utils'; +import { termQuery } from '@kbn/observability-plugin/server'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { getOverallLatencyDistribution } from './get_overall_latency_distribution'; import { setupRequest } from '../../lib/helpers/setup_request'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { environmentRt, kueryRt, rangeRt } from '../default_api_types'; +import { + SERVICE_NAME, + TRANSACTION_NAME, + TRANSACTION_TYPE, +} from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; -const latencyOverallDistributionRoute = createApmServerRoute({ - endpoint: 'POST /internal/apm/latency/overall_distribution', +const latencyOverallTransactionDistributionRoute = createApmServerRoute({ + endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', params: t.type({ body: t.intersection([ t.partial({ @@ -54,19 +62,29 @@ const latencyOverallDistributionRoute = createApmServerRoute({ } = resources.params.body; return getOverallLatencyDistribution({ + setup, + eventType: ProcessorEvent.transaction, environment, kuery, - serviceName, - transactionType, - transactionName, start, end, + query: { + bool: { + filter: [ + ...termQuery(SERVICE_NAME, serviceName), + ...termQuery(TRANSACTION_TYPE, transactionType), + ...termQuery(TRANSACTION_NAME, transactionName), + ...(termFilters?.flatMap( + (fieldValuePair): QueryDslQueryContainer[] => + termQuery(fieldValuePair.fieldName, fieldValuePair.fieldValue) + ) ?? []), + ], + }, + }, percentileThreshold, - termFilters, - setup, }); }, }); export const latencyDistributionRouteRepository = - latencyOverallDistributionRoute; + latencyOverallTransactionDistributionRoute; diff --git a/x-pack/plugins/apm/server/routes/latency_distribution/types.ts b/x-pack/plugins/apm/server/routes/latency_distribution/types.ts index d2eb9cde0bf3b..c5f394e4dd9e6 100644 --- a/x-pack/plugins/apm/server/routes/latency_distribution/types.ts +++ b/x-pack/plugins/apm/server/routes/latency_distribution/types.ts @@ -5,22 +5,20 @@ * 2.0. */ -import type { - FieldValuePair, - CorrelationsClientParams, -} from '../../../common/correlations/types'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { FieldValuePair } from '../../../common/correlations/types'; import { Setup } from '../../lib/helpers/setup_request'; -export interface OverallLatencyDistributionOptions - extends CorrelationsClientParams { +export interface OverallLatencyDistributionOptions { + query: QueryDslQueryContainer; percentileThreshold: number; termFilters?: FieldValuePair[]; setup: Setup; } export interface OverallLatencyDistributionResponse { - percentileThresholdValue?: number; + percentileThresholdValue?: number | null; overallHistogram?: Array<{ key: number; doc_count: number; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index ae829d05815fb..59c45ebb3e98b 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -8010,13 +8010,13 @@ "xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel": "Afficher la transaction dans Discover", "xpack.apm.transactionBreakdown.chartHelp": "La durée moyenne de chaque type d'intervalle. \"app\" indique que quelque chose se passait au sein du service - cela peut signifier que le temps a été passé dans le code de l'application et non dans la base de données ou les requêtes externes, ou que l'auto-instrumentation de l'agent APM ne couvre pas le code exécuté.", "xpack.apm.transactionBreakdown.chartTitle": "Temps consacré par type d'intervalle", - "xpack.apm.transactionDetails.clearSelectionAriaLabel": "Effacer la sélection", + "xpack.apm.durationDistributionChartWithScrubber.clearSelectionAriaLabel": "Effacer la sélection", "xpack.apm.transactionDetails.coldstartBadge": "démarrage à froid", "xpack.apm.transactionDetails.distribution.failedTransactionsLatencyDistributionErrorTitle": "Une erreur s'est produite lors de la récupération de la distribution de la latence des transactions ayant échoué.", "xpack.apm.transactionDetails.distribution.latencyDistributionErrorTitle": "Une erreur s'est produite lors de la récupération de la distribution de la latence globale.", - "xpack.apm.transactionDetails.distribution.panelTitle": "Distribution de la latence", - "xpack.apm.transactionDetails.distribution.selectionText": "Selection : {formattedSelection}", - "xpack.apm.transactionDetails.emptySelectionText": "Glisser et déposer pour sélectionner une plage", + "xpack.apm.durationDistributionChartWithScrubber.panelTitle": "Distribution de la latence", + "xpack.apm.durationDistributionChartWithScrubber.selectionText": "Selection : {formattedSelection}", + "xpack.apm.durationDistributionChartWithScrubber.emptySelectionText": "Glisser et déposer pour sélectionner une plage", "xpack.apm.transactionDetails.errorCount": "{errorCount, number} {errorCount, plural, one {erreur} other {erreurs}}", "xpack.apm.transactionDetails.noTraceParentButtonTooltip": "Le parent de la trace n'a pas pu être trouvé", "xpack.apm.transactionDetails.percentOfTraceLabelExplanation": "Le % de {parentType, select, transaction {transaction} trace {trace} } dépasse 100 %, car {childType, select, span {cet intervalle} transaction {cette transaction} } prend plus de temps que la transaction racine.", @@ -8052,11 +8052,10 @@ "xpack.apm.transactionDetails.viewFullTraceButtonLabel": "Afficher la trace complète", "xpack.apm.transactionDetails.viewingFullTraceButtonTooltip": "Affichage actuel de la trace complète", "xpack.apm.transactionDistribution.chart.allTransactionsLabel": "Toutes les transactions", - "xpack.apm.transactionDistribution.chart.currentTransactionMarkerLabel": "Échantillon actuel", "xpack.apm.transactionDistribution.chart.failedTransactionsLabel": "Transactions ayant échoué", - "xpack.apm.transactionDistribution.chart.latencyLabel": "Latence", - "xpack.apm.transactionDistribution.chart.numberOfTransactionsLabel": "Transactions", - "xpack.apm.transactionDistribution.chart.percentileMarkerLabel": "{markerPercentile}e centile", + "xpack.apm.durationDistribution.chart.latencyLabel": "Latence", + "xpack.apm.durationDistribution.chart.numberOfTransactionsLabel": "Transactions", + "xpack.apm.durationDistribution.chart.percentileMarkerLabel": "{markerPercentile}e centile", "xpack.apm.transactionDurationAlert.aggregationType.95th": "95e centile", "xpack.apm.transactionDurationAlert.aggregationType.99th": "99e centile", "xpack.apm.transactionDurationAlert.aggregationType.avg": "Moyenne", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e8e2fc397bf63..dfb400097a5f9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8109,13 +8109,13 @@ "xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel": "Discoverでトランザクションを表示", "xpack.apm.transactionBreakdown.chartHelp": "各スパンタイプの平均期間。「app」はサービス内で何かが発生していたことを示します。これは、データベースや外部リクエストではなく、アプリケーションコードで時間がかかったこと、またはAPMエージェントの自動計測で実行されたコードが明らかにならないことを意味する場合があります。", "xpack.apm.transactionBreakdown.chartTitle": "スパンタイプ別時間", - "xpack.apm.transactionDetails.clearSelectionAriaLabel": "選択した項目をクリア", + "xpack.apm.durationDistributionChartWithScrubber.clearSelectionAriaLabel": "選択した項目をクリア", "xpack.apm.transactionDetails.coldstartBadge": "コールドスタート", "xpack.apm.transactionDetails.distribution.failedTransactionsLatencyDistributionErrorTitle": "失敗したトランザクション遅延分布の取得中にエラーが発生しました。", "xpack.apm.transactionDetails.distribution.latencyDistributionErrorTitle": "全体の遅延分布の取得中にエラーが発生しました。", - "xpack.apm.transactionDetails.distribution.panelTitle": "レイテンシ分布", - "xpack.apm.transactionDetails.distribution.selectionText": "選択:{formattedSelection}", - "xpack.apm.transactionDetails.emptySelectionText": "クリックおよびドラッグして範囲を選択", + "xpack.apm.durationDistributionChartWithScrubber.panelTitle": "レイテンシ分布", + "xpack.apm.durationDistributionChartWithScrubber.selectionText": "選択:{formattedSelection}", + "xpack.apm.durationDistributionChartWithScrubber.emptySelectionText": "クリックおよびドラッグして範囲を選択", "xpack.apm.transactionDetails.errorCount": "{errorCount, number} {errorCount, plural, other {エラー}}", "xpack.apm.transactionDetails.noTraceParentButtonTooltip": "トレースの親が見つかりませんでした", "xpack.apm.transactionDetails.percentOfTraceLabelExplanation": "{parentType, select, transaction {トランザクション} trace {トレース} }の割合が100%を超えています。これは、この{childType, select, span {スパン} transaction {トランザクション} }がルートトランザクションよりも時間がかかるためです。", @@ -8151,10 +8151,9 @@ "xpack.apm.transactionDetails.viewFullTraceButtonLabel": "完全なトレースを表示", "xpack.apm.transactionDetails.viewingFullTraceButtonTooltip": "現在完全なトレースが表示されています", "xpack.apm.transactionDistribution.chart.allTransactionsLabel": "すべてのトランザクション", - "xpack.apm.transactionDistribution.chart.currentTransactionMarkerLabel": "現在のサンプル", "xpack.apm.transactionDistribution.chart.failedTransactionsLabel": "失敗したトランザクション", - "xpack.apm.transactionDistribution.chart.latencyLabel": "レイテンシ", - "xpack.apm.transactionDistribution.chart.numberOfTransactionsLabel": "トランザクション", + "xpack.apm.durationDistribution.chart.latencyLabel": "レイテンシ", + "xpack.apm.durationDistribution.chart.numberOfTransactionsLabel": "トランザクション", "xpack.apm.transactionDurationAlert.aggregationType.95th": "95 パーセンタイル", "xpack.apm.transactionDurationAlert.aggregationType.99th": "99 パーセンタイル", "xpack.apm.transactionDurationAlert.aggregationType.avg": "平均", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 09d512eae30db..1601b254b4d98 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8123,13 +8123,13 @@ "xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel": "在 Discover 中查看事务", "xpack.apm.transactionBreakdown.chartHelp": "每种跨度类型的平均持续时间。“应用”表示该服务内有情况发生 — 这可能指在应用程序代码而不是数据库或外部请求中花费的时间,或 APM 代理自动检测未覆盖已执行的代码。", "xpack.apm.transactionBreakdown.chartTitle": "跨度类型花费的时间", - "xpack.apm.transactionDetails.clearSelectionAriaLabel": "清除所选内容", + "xpack.apm.durationDistributionChartWithScrubber.clearSelectionAriaLabel": "清除所选内容", "xpack.apm.transactionDetails.coldstartBadge": "冷启动", "xpack.apm.transactionDetails.distribution.failedTransactionsLatencyDistributionErrorTitle": "提取失败事务延迟分布时出错。", "xpack.apm.transactionDetails.distribution.latencyDistributionErrorTitle": "提取总体延迟分布时出错。", - "xpack.apm.transactionDetails.distribution.panelTitle": "延迟分布", - "xpack.apm.transactionDetails.distribution.selectionText": "选择:{formattedSelection}", - "xpack.apm.transactionDetails.emptySelectionText": "单击并拖动以选择范围", + "xpack.apm.durationDistributionChartWithScrubber.panelTitle": "延迟分布", + "xpack.apm.durationDistributionChartWithScrubber.selectionText": "选择:{formattedSelection}", + "xpack.apm.durationDistributionChartWithScrubber.emptySelectionText": "单击并拖动以选择范围", "xpack.apm.transactionDetails.errorCount": "{errorCount, number} 个 {errorCount, plural, other {错误}}", "xpack.apm.transactionDetails.noTraceParentButtonTooltip": "找不到上级追溯", "xpack.apm.transactionDetails.percentOfTraceLabelExplanation": "{parentType, select, transaction {事务} trace {追溯} }的百分比超过 100%,因为此{childType, select, span {跨度} transaction {事务} }比根事务花费更长的时间。", @@ -8165,11 +8165,10 @@ "xpack.apm.transactionDetails.viewFullTraceButtonLabel": "查看完整追溯信息", "xpack.apm.transactionDetails.viewingFullTraceButtonTooltip": "当前正在查看完整追溯信息", "xpack.apm.transactionDistribution.chart.allTransactionsLabel": "所有事务", - "xpack.apm.transactionDistribution.chart.currentTransactionMarkerLabel": "当前样例", "xpack.apm.transactionDistribution.chart.failedTransactionsLabel": "失败事务", - "xpack.apm.transactionDistribution.chart.latencyLabel": "延迟", - "xpack.apm.transactionDistribution.chart.numberOfTransactionsLabel": "事务", - "xpack.apm.transactionDistribution.chart.percentileMarkerLabel": "第 {markerPercentile} 个百分位数", + "xpack.apm.durationDistribution.chart.latencyLabel": "延迟", + "xpack.apm.durationDistribution.chart.numberOfTransactionsLabel": "事务", + "xpack.apm.durationDistribution.chart.percentileMarkerLabel": "第 {markerPercentile} 个百分位数", "xpack.apm.transactionDurationAlert.aggregationType.95th": "第 95 个百分位", "xpack.apm.transactionDurationAlert.aggregationType.99th": "第 99 个百分位", "xpack.apm.transactionDurationAlert.aggregationType.avg": "平均值", diff --git a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.spec.ts b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.spec.ts index 8fedcad2a6979..2c8e89528c45a 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.spec.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.spec.ts @@ -29,7 +29,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('failed transactions without data', { config: 'trial', archives: [] }, () => { it('handles the empty state', async () => { const overallDistributionResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/latency/overall_distribution', + endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', params: { body: { ...getOptions(), @@ -44,7 +44,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); const errorDistributionResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/latency/overall_distribution', + endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', params: { body: { ...getOptions(), @@ -60,7 +60,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); const fieldCandidatesResponse = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/correlations/field_candidates', + endpoint: 'GET /internal/apm/correlations/field_candidates/transactions', params: { query: getOptions(), }, @@ -72,7 +72,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); const failedTransactionsCorrelationsResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/correlations/p_values', + endpoint: 'POST /internal/apm/correlations/p_values/transactions', params: { body: { ...getOptions(), @@ -104,7 +104,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('failed transactions with data', { config: 'trial', archives: ['8.0.0'] }, () => { it('runs queries and returns results', async () => { const overallDistributionResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/latency/overall_distribution', + endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', params: { body: { ...getOptions(), @@ -119,7 +119,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); const errorDistributionResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/latency/overall_distribution', + endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', params: { body: { ...getOptions(), @@ -135,7 +135,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); const fieldCandidatesResponse = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/correlations/field_candidates', + endpoint: 'GET /internal/apm/correlations/field_candidates/transactions', params: { query: getOptions(), }, @@ -157,7 +157,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); const failedTransactionsCorrelationsResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/correlations/p_values', + endpoint: 'POST /internal/apm/correlations/p_values/transactions', params: { body: { ...getOptions(), @@ -179,7 +179,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { } const failedtransactionsFieldStats = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/correlations/field_stats', + endpoint: 'POST /internal/apm/correlations/field_stats/transactions', params: { body: { ...getOptions(), diff --git a/x-pack/test/apm_api_integration/tests/correlations/field_candidates.spec.ts b/x-pack/test/apm_api_integration/tests/correlations/field_candidates.spec.ts index a62145da25326..448f90b1228b4 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/field_candidates.spec.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/field_candidates.spec.ts @@ -12,7 +12,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const apmApiClient = getService('apmApiClient'); const registry = getService('registry'); - const endpoint = 'GET /internal/apm/correlations/field_candidates'; + const endpoint = 'GET /internal/apm/correlations/field_candidates/transactions'; const getOptions = () => ({ params: { diff --git a/x-pack/test/apm_api_integration/tests/correlations/field_value_pairs.spec.ts b/x-pack/test/apm_api_integration/tests/correlations/field_value_pairs.spec.ts index df9314546d6de..4765e83342e52 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/field_value_pairs.spec.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/field_value_pairs.spec.ts @@ -12,7 +12,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const apmApiClient = getService('apmApiClient'); const registry = getService('registry'); - const endpoint = 'POST /internal/apm/correlations/field_value_pairs'; + const endpoint = 'POST /internal/apm/correlations/field_value_pairs/transactions'; const getOptions = () => ({ params: { diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency.spec.ts b/x-pack/test/apm_api_integration/tests/correlations/latency.spec.ts index 3e46cd9c7c2b3..c9b6f1a341a06 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/latency.spec.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/latency.spec.ts @@ -35,7 +35,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { () => { it('handles the empty state', async () => { const overallDistributionResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/latency/overall_distribution', + endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', params: { body: { ...getOptions(), @@ -50,7 +50,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); const fieldCandidatesResponse = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/correlations/field_candidates', + endpoint: 'GET /internal/apm/correlations/field_candidates/transactions', params: { query: getOptions(), }, @@ -62,7 +62,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); const fieldValuePairsResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/correlations/field_value_pairs', + endpoint: 'POST /internal/apm/correlations/field_value_pairs/transactions', params: { body: { ...getOptions(), @@ -77,7 +77,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); const significantCorrelationsResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/correlations/significant_correlations', + endpoint: 'POST /internal/apm/correlations/significant_correlations/transactions', params: { body: { ...getOptions(), @@ -112,7 +112,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { // putting this into a single `it` because the responses depend on each other it('runs queries and returns results', async () => { const overallDistributionResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/latency/overall_distribution', + endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', params: { body: { ...getOptions(), @@ -127,7 +127,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); const fieldCandidatesResponse = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/correlations/field_candidates', + endpoint: 'GET /internal/apm/correlations/field_candidates/transactions', params: { query: getOptions(), }, @@ -145,7 +145,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); const fieldValuePairsResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/correlations/field_value_pairs', + endpoint: 'POST /internal/apm/correlations/field_value_pairs/transactions', params: { body: { ...getOptions(), @@ -181,7 +181,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { for (const fieldValuePairChunk of fieldValuePairChunks) { const significantCorrelations = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/correlations/significant_correlations', + endpoint: 'POST /internal/apm/correlations/significant_correlations/transactions', params: { body: { ...getOptions(), @@ -218,7 +218,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { } const failedtransactionsFieldStats = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/correlations/field_stats', + endpoint: 'POST /internal/apm/correlations/field_stats/transactions', params: { body: { ...getOptions(), diff --git a/x-pack/test/apm_api_integration/tests/correlations/p_values.spec.ts b/x-pack/test/apm_api_integration/tests/correlations/p_values.spec.ts index 1f3dd58063087..42a9fdadbb480 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/p_values.spec.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/p_values.spec.ts @@ -12,7 +12,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const apmApiClient = getService('apmApiClient'); const registry = getService('registry'); - const endpoint = 'POST /internal/apm/correlations/p_values'; + const endpoint = 'POST /internal/apm/correlations/p_values/transactions'; const getOptions = () => ({ params: { diff --git a/x-pack/test/apm_api_integration/tests/correlations/significant_correlations.spec.ts b/x-pack/test/apm_api_integration/tests/correlations/significant_correlations.spec.ts index 994f23bbf2a4e..d4450c192a029 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/significant_correlations.spec.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/significant_correlations.spec.ts @@ -12,7 +12,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const apmApiClient = getService('apmApiClient'); const registry = getService('registry'); - const endpoint = 'POST /internal/apm/correlations/significant_correlations'; + const endpoint = 'POST /internal/apm/correlations/significant_correlations/transactions'; const getOptions = () => ({ params: { diff --git a/x-pack/test/apm_api_integration/tests/dependencies/top_spans.spec.ts b/x-pack/test/apm_api_integration/tests/dependencies/top_spans.spec.ts index 6b442af134380..5e497c969e920 100644 --- a/x-pack/test/apm_api_integration/tests/dependencies/top_spans.spec.ts +++ b/x-pack/test/apm_api_integration/tests/dependencies/top_spans.spec.ts @@ -23,11 +23,15 @@ export default function ApiTest({ getService }: FtrProviderContext) { spanName, kuery = '', environment = ENVIRONMENT_ALL.value, + sampleRangeFrom, + sampleRangeTo, }: { backendName: string; spanName: string; kuery?: string; environment?: string; + sampleRangeFrom?: number; + sampleRangeTo?: number; }) { return await apmApiClient.readUser({ endpoint: `GET /internal/apm/backends/operations/spans`, @@ -39,6 +43,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { environment, kuery, spanName, + sampleRangeFrom, + sampleRangeTo, }, }, }); @@ -88,6 +94,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .span('/_search', 'db', 'elasticsearch') .destination('elasticsearch') .duration(100) + .success() .timestamp(timestamp) ), goInstance @@ -144,6 +151,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { spanName: '/_search', transactionName: 'GET /api/my-endpoint', transactionType: 'request', + outcome: 'success', }); expect(omit(goSpans[0], 'traceId', 'transactionId')).to.eql({ @@ -154,6 +162,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { spanName: '/_search', transactionName: 'GET /api/my-other-endpoint', transactionType: 'request', + outcome: 'unknown', }); }); }); @@ -221,6 +230,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { duration: 200000, serviceName: 'java', spanName: 'without transaction', + outcome: 'unknown', }); expect(spans[0].transactionType).not.to.be.ok(); @@ -229,6 +239,21 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); + describe('when requesting spans within a specific sample range', () => { + it('returns only spans whose duration falls into the requested range', async () => { + const response = await callApi({ + backendName: 'elasticsearch', + spanName: '/_search', + sampleRangeFrom: 50000, + sampleRangeTo: 99999, + }); + + const { spans } = response.body; + + expect(spans.every((span) => span.duration === 50000)).to.be(true); + }); + }); + after(() => synthtraceEsClient.clean()); } ); diff --git a/x-pack/test/apm_api_integration/tests/transactions/latency_overall_distribution.spec.ts b/x-pack/test/apm_api_integration/tests/transactions/latency_overall_distribution.spec.ts index ff1a49fb8fc54..eaaea72de5539 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/latency_overall_distribution.spec.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/latency_overall_distribution.spec.ts @@ -12,7 +12,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); - const endpoint = 'POST /internal/apm/latency/overall_distribution'; + const endpoint = 'POST /internal/apm/latency/overall_distribution/transactions'; // This matches the parameters used for the other tab's search strategy approach in `../correlations/*`. const getOptions = () => ({ From 8fa2608172e10b4719f94ee245cd745042276a2d Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Thu, 23 Jun 2022 07:09:46 -0700 Subject: [PATCH 26/54] [XY] Add `axes` support (#129476) * Added `axes` support * Refactoring auto-assignment logic * Fixed reference line position * Fixed bug with auto-assignment. * Fixed snapshots * Fixed behavior of the horizontal reference lines. Co-authored-by: Yaroslav Kuznietsov Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Marta Bondyra --- .../expression_xy/common/__mocks__/index.ts | 78 ++- .../expression_xy/common/constants.ts | 26 +- .../axis_titles_visibility_config.ts | 53 -- .../expression_functions/common_axis_args.ts | 72 ++ .../common_data_layer_args.ts | 23 +- .../expression_functions/common_xy_args.ts | 72 +- .../common_y_config_args.ts | 13 +- .../data_decoration_config.ts | 37 + .../extended_data_layer.test.ts | 3 + .../grid_lines_config.test.ts | 20 - .../expression_functions/grid_lines_config.ts | 53 -- .../common/expression_functions/index.ts | 8 +- .../labels_orientation_config.test.ts | 20 - .../labels_orientation_config.ts | 56 -- .../expression_functions/layered_xy_vis_fn.ts | 3 + .../reference_line.test.ts | 26 +- .../expression_functions/reference_line.ts | 36 +- ...ts => reference_line_decoration_config.ts} | 41 +- .../reference_line_layer.ts | 8 +- .../tick_labels_config.test.ts | 20 - .../tick_labels_config.ts | 53 -- .../common/expression_functions/validate.ts | 76 ++- .../expression_functions/x_axis_config.ts | 37 + .../expression_functions/xy_vis.test.ts | 52 +- .../common/expression_functions/xy_vis_fn.ts | 19 +- .../expression_functions/y_axis_config.ts | 42 +- .../common/helpers/layers.test.ts | 3 + .../expression_xy/common/helpers/layers.ts | 6 +- .../common/helpers/visualization.test.ts | 3 + .../expression_xy/common/i18n/index.tsx | 164 +++-- .../expression_xy/common/index.ts | 16 +- .../common/types/expression_functions.ts | 158 ++--- .../common/types/expression_renderers.ts | 2 +- .../expression_xy/public/__mocks__/index.tsx | 14 +- .../__snapshots__/xy_chart.test.tsx.snap | 613 ++++++++++++++++- .../public/components/annotations.tsx | 2 +- .../public/components/data_layers.tsx | 25 +- .../public/components/legend_action.test.tsx | 3 + .../reference_lines/reference_line.tsx | 28 +- .../reference_line_annotations.tsx | 37 +- .../reference_lines/reference_line_layer.tsx | 59 +- .../reference_lines/reference_lines.test.tsx | 190 ++++-- .../reference_lines/reference_lines.tsx | 16 +- .../components/reference_lines/utils.tsx | 71 +- .../public/components/xy_chart.test.tsx | 631 ++++++++++++------ .../public/components/xy_chart.tsx | 256 ++++--- .../public/definitions/visualizations.ts | 14 +- .../public/helpers/annotations.tsx | 43 +- .../public/helpers/axes_configuration.test.ts | 44 +- .../public/helpers/axes_configuration.ts | 226 +++++-- .../public/helpers/color_assignment.test.ts | 6 + .../public/helpers/data_layers.tsx | 19 +- .../expression_xy/public/helpers/layers.ts | 32 +- .../expression_xy/public/helpers/state.ts | 28 +- .../public/helpers/visualization.ts | 13 +- .../expression_xy/public/plugin.ts | 16 +- .../expression_xy/server/plugin.ts | 18 +- x-pack/plugins/lens/public/index.ts | 12 +- .../shared_components/axis_title_settings.tsx | 2 +- .../__snapshots__/to_expression.test.ts.snap | 218 +++--- .../reference_line_helpers.tsx | 11 +- .../public/xy_visualization/state_helpers.ts | 6 +- .../xy_visualization/to_expression.test.ts | 102 ++- .../public/xy_visualization/to_expression.ts | 196 +++--- .../lens/public/xy_visualization/types.ts | 62 +- .../xy_visualization/visualization.test.ts | 2 +- .../public/xy_visualization/visualization.tsx | 22 +- .../visualization_helpers.tsx | 2 +- .../xy_config_panel/axis_settings_popover.tsx | 4 +- .../xy_config_panel/dimension_editor.tsx | 5 +- .../xy_config_panel/index.tsx | 4 +- .../xy_config_panel/layer_header.tsx | 3 +- .../reference_line_panel.tsx | 12 +- .../shared/marker_decoration_settings.tsx | 3 +- .../public/xy_visualization/xy_suggestions.ts | 10 +- .../configurations/lens_attributes.ts | 5 +- .../shared/exploratory_view/types.ts | 4 +- .../translations/translations/fr-FR.json | 37 - .../translations/translations/ja-JP.json | 37 - .../translations/translations/zh-CN.json | 37 - .../apps/lens/group1/smokescreen.ts | 4 +- 81 files changed, 2775 insertions(+), 1728 deletions(-) delete mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/axis_titles_visibility_config.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/common_axis_args.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/data_decoration_config.ts delete mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/grid_lines_config.test.ts delete mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/grid_lines_config.ts delete mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/labels_orientation_config.test.ts delete mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/labels_orientation_config.ts rename src/plugins/chart_expressions/expression_xy/common/expression_functions/{extended_y_axis_config.ts => reference_line_decoration_config.ts} (55%) delete mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/tick_labels_config.test.ts delete mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/tick_labels_config.ts create mode 100644 src/plugins/chart_expressions/expression_xy/common/expression_functions/x_axis_config.ts diff --git a/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts b/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts index e9810d2764025..1bde83c1822b7 100644 --- a/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts +++ b/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts @@ -58,6 +58,9 @@ export const sampleLayer: DataLayerConfig = { columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', xScaleType: 'ordinal', isHistogram: false, + isHorizontal: false, + isPercentage: false, + isStacked: false, palette: mockPaletteOutput, table: createSampleDatatableWithRows([]), }; @@ -73,6 +76,9 @@ export const sampleExtendedLayer: ExtendedDataLayerConfig = { columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', xScaleType: 'ordinal', isHistogram: false, + isHorizontal: false, + isStacked: false, + isPercentage: false, palette: mockPaletteOutput, table: createSampleDatatableWithRows([]), }; @@ -80,9 +86,6 @@ export const sampleExtendedLayer: ExtendedDataLayerConfig = { export const createArgsWithLayers = ( layers: DataLayerConfig | DataLayerConfig[] = sampleLayer ): XYProps => ({ - xTitle: '', - yTitle: '', - yRightTitle: '', showTooltip: true, legend: { type: 'legendConfig', @@ -91,41 +94,44 @@ export const createArgsWithLayers = ( }, valueLabels: 'hide', valuesInLegend: false, - axisTitlesVisibilitySettings: { - type: 'axisTitlesVisibilityConfig', - x: true, - yLeft: true, - yRight: true, - }, - tickLabelsVisibilitySettings: { - type: 'tickLabelsConfig', - x: true, - yLeft: false, - yRight: false, - }, - labelsOrientation: { - type: 'labelsOrientationConfig', - x: 0, - yLeft: -90, - yRight: -45, - }, - gridlinesVisibilitySettings: { - type: 'gridlinesConfig', - x: true, - yLeft: false, - yRight: false, - }, - yLeftExtent: { - mode: 'full', - type: 'axisExtentConfig', - }, - yRightExtent: { - mode: 'full', - type: 'axisExtentConfig', + xAxisConfig: { + type: 'xAxisConfig', + position: 'bottom', + showGridLines: true, + labelsOrientation: 0, + showLabels: true, + showTitle: true, + title: '', }, + yAxisConfigs: [ + { + type: 'yAxisConfig', + position: 'right', + showGridLines: false, + labelsOrientation: -45, + showLabels: false, + showTitle: true, + title: '', + extent: { + mode: 'full', + type: 'axisExtentConfig', + }, + }, + { + type: 'yAxisConfig', + position: 'left', + showGridLines: false, + labelsOrientation: -90, + showLabels: false, + showTitle: true, + title: '', + extent: { + mode: 'full', + type: 'axisExtentConfig', + }, + }, + ], layers: Array.isArray(layers) ? layers : [layers], - yLeftScale: 'linear', - yRightScale: 'linear', }); export function sampleArgs() { diff --git a/src/plugins/chart_expressions/expression_xy/common/constants.ts b/src/plugins/chart_expressions/expression_xy/common/constants.ts index fc2e41700b94f..5d9d7fb70d478 100644 --- a/src/plugins/chart_expressions/expression_xy/common/constants.ts +++ b/src/plugins/chart_expressions/expression_xy/common/constants.ts @@ -8,22 +8,20 @@ export const XY_VIS = 'xyVis'; export const LAYERED_XY_VIS = 'layeredXyVis'; -export const Y_CONFIG = 'yConfig'; -export const REFERENCE_LINE_Y_CONFIG = 'referenceLineYConfig'; -export const EXTENDED_Y_CONFIG = 'extendedYConfig'; +export const DATA_DECORATION_CONFIG = 'dataDecorationConfig'; +export const REFERENCE_LINE_DECORATION_CONFIG = 'referenceLineDecorationConfig'; +export const EXTENDED_REFERENCE_LINE_DECORATION_CONFIG = 'extendedReferenceLineDecorationConfig'; +export const X_AXIS_CONFIG = 'xAxisConfig'; +export const Y_AXIS_CONFIG = 'yAxisConfig'; export const DATA_LAYER = 'dataLayer'; export const EXTENDED_DATA_LAYER = 'extendedDataLayer'; export const LEGEND_CONFIG = 'legendConfig'; export const XY_VIS_RENDERER = 'xyVis'; -export const GRID_LINES_CONFIG = 'gridlinesConfig'; export const ANNOTATION_LAYER = 'annotationLayer'; export const EXTENDED_ANNOTATION_LAYER = 'extendedAnnotationLayer'; -export const TICK_LABELS_CONFIG = 'tickLabelsConfig'; export const AXIS_EXTENT_CONFIG = 'axisExtentConfig'; export const REFERENCE_LINE = 'referenceLine'; export const REFERENCE_LINE_LAYER = 'referenceLineLayer'; -export const LABELS_ORIENTATION_CONFIG = 'labelsOrientationConfig'; -export const AXIS_TITLES_VISIBILITY_CONFIG = 'axisTitlesVisibilityConfig'; export const LayerTypes = { DATA: 'data', @@ -82,13 +80,6 @@ export const SeriesTypes = { BAR: 'bar', LINE: 'line', AREA: 'area', - BAR_STACKED: 'bar_stacked', - AREA_STACKED: 'area_stacked', - BAR_HORIZONTAL: 'bar_horizontal', - BAR_PERCENTAGE_STACKED: 'bar_percentage_stacked', - BAR_HORIZONTAL_STACKED: 'bar_horizontal_stacked', - AREA_PERCENTAGE_STACKED: 'area_percentage_stacked', - BAR_HORIZONTAL_PERCENTAGE_STACKED: 'bar_horizontal_percentage_stacked', } as const; export const YScaleTypes = { @@ -131,3 +122,10 @@ export const AvailableReferenceLineIcons = { TAG: 'tag', TRIANGLE: 'triangle', } as const; + +export const AxisModes = { + NORMAL: 'normal', + PERCENTAGE: 'percentage', + WIGGLE: 'wiggle', + SILHOUETTE: 'silhouette', +} as const; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/axis_titles_visibility_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/axis_titles_visibility_config.ts deleted file mode 100644 index 53b9ca238fe78..0000000000000 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/axis_titles_visibility_config.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; -import { AXIS_TITLES_VISIBILITY_CONFIG } from '../constants'; -import { AxesSettingsConfig, AxisTitlesVisibilityConfigResult } from '../types'; - -export const axisTitlesVisibilityConfigFunction: ExpressionFunctionDefinition< - typeof AXIS_TITLES_VISIBILITY_CONFIG, - null, - AxesSettingsConfig, - AxisTitlesVisibilityConfigResult -> = { - name: AXIS_TITLES_VISIBILITY_CONFIG, - aliases: [], - type: AXIS_TITLES_VISIBILITY_CONFIG, - help: i18n.translate('expressionXY.axisTitlesVisibilityConfig.help', { - defaultMessage: `Configure the xy chart's axis titles appearance`, - }), - inputTypes: ['null'], - args: { - x: { - types: ['boolean'], - help: i18n.translate('expressionXY.axisTitlesVisibilityConfig.x.help', { - defaultMessage: 'Specifies whether or not the title of the x-axis are visible.', - }), - }, - yLeft: { - types: ['boolean'], - help: i18n.translate('expressionXY.axisTitlesVisibilityConfig.yLeft.help', { - defaultMessage: 'Specifies whether or not the title of the left y-axis are visible.', - }), - }, - yRight: { - types: ['boolean'], - help: i18n.translate('expressionXY.axisTitlesVisibilityConfig.yRight.help', { - defaultMessage: 'Specifies whether or not the title of the right y-axis are visible.', - }), - }, - }, - fn(inputn, args) { - return { - type: AXIS_TITLES_VISIBILITY_CONFIG, - ...args, - }; - }, -}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_axis_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_axis_args.ts new file mode 100644 index 0000000000000..942d83c27ea75 --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_axis_args.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { strings } from '../i18n'; +import { XAxisConfigFn, YAxisConfigFn } from '../types'; +import { AXIS_EXTENT_CONFIG } from '../constants'; + +type CommonAxisConfigFn = XAxisConfigFn | YAxisConfigFn; + +export const commonAxisConfigArgs: Omit< + CommonAxisConfigFn['args'], + 'scaleType' | 'mode' | 'boundsMargin' +> = { + title: { + types: ['string'], + help: strings.getAxisTitleHelp(), + }, + id: { + types: ['string'], + help: strings.getAxisIdHelp(), + }, + hide: { + types: ['boolean'], + help: strings.getAxisHideHelp(), + }, + labelColor: { + types: ['string'], + help: strings.getAxisLabelColorHelp(), + }, + showOverlappingLabels: { + types: ['boolean'], + help: strings.getAxisShowOverlappingLabelsHelp(), + }, + showDuplicates: { + types: ['boolean'], + help: strings.getAxisShowDuplicatesHelp(), + }, + showGridLines: { + types: ['boolean'], + help: strings.getAxisShowGridLinesHelp(), + default: false, + }, + labelsOrientation: { + types: ['number'], + options: [0, -90, -45], + help: strings.getAxisLabelsOrientationHelp(), + }, + showLabels: { + types: ['boolean'], + help: strings.getAxisShowLabelsHelp(), + default: true, + }, + showTitle: { + types: ['boolean'], + help: strings.getAxisShowTitleHelp(), + default: true, + }, + truncate: { + types: ['number'], + help: strings.getAxisTruncateHelp(), + }, + extent: { + types: [AXIS_EXTENT_CONFIG], + help: strings.getAxisExtentHelp(), + default: `{${AXIS_EXTENT_CONFIG}}`, + }, +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts index ffa3a3ec614fa..330e31327873a 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts @@ -7,7 +7,7 @@ */ import { ArgumentType } from '@kbn/expressions-plugin/common'; -import { SeriesTypes, XScaleTypes, Y_CONFIG } from '../constants'; +import { SeriesTypes, XScaleTypes, DATA_DECORATION_CONFIG } from '../constants'; import { strings } from '../i18n'; import { DataLayerArgs, ExtendedDataLayerArgs } from '../types'; @@ -43,6 +43,21 @@ export const commonDataLayerArgs: Omit< default: false, help: strings.getIsHistogramHelp(), }, + isPercentage: { + types: ['boolean'], + default: false, + help: strings.getIsPercentageHelp(), + }, + isStacked: { + types: ['boolean'], + default: false, + help: strings.getIsStackedHelp(), + }, + isHorizontal: { + types: ['boolean'], + default: false, + help: strings.getIsHorizontalHelp(), + }, lineWidth: { types: ['number'], help: strings.getLineWidthHelp(), @@ -59,9 +74,9 @@ export const commonDataLayerArgs: Omit< types: ['boolean'], help: strings.getShowLinesHelp(), }, - yConfig: { - types: [Y_CONFIG], - help: strings.getYConfigHelp(), + decorations: { + types: [DATA_DECORATION_CONFIG], + help: strings.getDecorationsHelp(), multi: true, }, columnToLabel: { diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts index ceb24da6a3ec4..df9c4abdfe22e 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_xy_args.ts @@ -7,17 +7,13 @@ */ import { - AXIS_EXTENT_CONFIG, - AXIS_TITLES_VISIBILITY_CONFIG, EndValues, FittingFunctions, - GRID_LINES_CONFIG, - LABELS_ORIENTATION_CONFIG, LEGEND_CONFIG, - TICK_LABELS_CONFIG, ValueLabelModes, XYCurveTypes, - YScaleTypes, + X_AXIS_CONFIG, + Y_AXIS_CONFIG, } from '../constants'; import { strings } from '../i18n'; import { LayeredXyVisFn, XyVisFn } from '../types'; @@ -25,45 +21,6 @@ import { LayeredXyVisFn, XyVisFn } from '../types'; type CommonXYFn = XyVisFn | LayeredXyVisFn; export const commonXYArgs: CommonXYFn['args'] = { - xTitle: { - types: ['string'], - help: strings.getXTitleHelp(), - }, - yTitle: { - types: ['string'], - help: strings.getYTitleHelp(), - }, - yRightTitle: { - types: ['string'], - help: strings.getYRightTitleHelp(), - }, - xExtent: { - types: [AXIS_EXTENT_CONFIG], - help: strings.getXExtentHelp(), - default: `{${AXIS_EXTENT_CONFIG}}`, - }, - yLeftExtent: { - types: [AXIS_EXTENT_CONFIG], - help: strings.getYLeftExtentHelp(), - default: `{${AXIS_EXTENT_CONFIG}}`, - }, - yRightExtent: { - types: [AXIS_EXTENT_CONFIG], - help: strings.getYRightExtentHelp(), - default: `{${AXIS_EXTENT_CONFIG}}`, - }, - yLeftScale: { - options: [...Object.values(YScaleTypes)], - help: strings.getYLeftScaleTypeHelp(), - default: YScaleTypes.LINEAR, - strict: true, - }, - yRightScale: { - options: [...Object.values(YScaleTypes)], - help: strings.getYRightScaleTypeHelp(), - default: YScaleTypes.LINEAR, - strict: true, - }, legend: { types: [LEGEND_CONFIG], help: strings.getLegendHelp(), @@ -93,22 +50,6 @@ export const commonXYArgs: CommonXYFn['args'] = { strict: true, default: ValueLabelModes.HIDE, }, - tickLabelsVisibilitySettings: { - types: [TICK_LABELS_CONFIG], - help: strings.getTickLabelsVisibilitySettingsHelp(), - }, - labelsOrientation: { - types: [LABELS_ORIENTATION_CONFIG], - help: strings.getLabelsOrientationHelp(), - }, - gridlinesVisibilitySettings: { - types: [GRID_LINES_CONFIG], - help: strings.getGridlinesVisibilitySettingsHelp(), - }, - axisTitlesVisibilitySettings: { - types: [AXIS_TITLES_VISIBILITY_CONFIG], - help: strings.getAxisTitlesVisibilitySettingsHelp(), - }, curveType: { types: ['string'], options: [...Object.values(XYCurveTypes)], @@ -133,6 +74,15 @@ export const commonXYArgs: CommonXYFn['args'] = { types: ['string'], help: strings.getAriaLabelHelp(), }, + xAxisConfig: { + types: [X_AXIS_CONFIG], + help: strings.getXAxisConfigHelp(), + }, + yAxisConfigs: { + types: [Y_AXIS_CONFIG], + help: strings.getyAxisConfigsHelp(), + multi: true, + }, detailedTooltip: { types: ['boolean'], help: strings.getDetailedTooltipHelp(), diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_y_config_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_y_config_args.ts index 76ac6ba2a1a97..dd6a1ae2f113a 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_y_config_args.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_y_config_args.ts @@ -6,22 +6,19 @@ * Side Public License, v 1. */ -import { YAxisModes } from '../constants'; import { strings } from '../i18n'; -import { YConfigFn, ExtendedYConfigFn } from '../types'; +import { DataDecorationConfigFn, ReferenceLineDecorationConfigFn } from '../types'; -type CommonYConfigFn = YConfigFn | ExtendedYConfigFn; +type CommonDecorationConfigFn = DataDecorationConfigFn | ReferenceLineDecorationConfigFn; -export const commonYConfigArgs: CommonYConfigFn['args'] = { +export const commonDecorationConfigArgs: CommonDecorationConfigFn['args'] = { forAccessor: { types: ['string'], help: strings.getForAccessorHelp(), }, - axisMode: { + axisId: { types: ['string'], - options: [...Object.values(YAxisModes)], - help: strings.getAxisModeHelp(), - strict: true, + help: strings.getAxisIdHelp(), }, color: { types: ['string'], diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_decoration_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_decoration_config.ts new file mode 100644 index 0000000000000..12eb8e578cc1f --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_decoration_config.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; +import { DATA_DECORATION_CONFIG } from '../constants'; +import { DataDecorationConfig, DataDecorationConfigResult } from '../types'; +import { commonDecorationConfigArgs } from './common_y_config_args'; + +export const dataDecorationConfigFunction: ExpressionFunctionDefinition< + typeof DATA_DECORATION_CONFIG, + null, + DataDecorationConfig, + DataDecorationConfigResult +> = { + name: DATA_DECORATION_CONFIG, + aliases: [], + type: DATA_DECORATION_CONFIG, + help: i18n.translate('expressionXY.dataDecorationConfig.help', { + defaultMessage: `Configure the decoration of data`, + }), + inputTypes: ['null'], + args: { + ...commonDecorationConfigArgs, + }, + fn(input, args) { + return { + type: DATA_DECORATION_CONFIG, + ...args, + }; + }, +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.test.ts index 56163d165b458..5ec11188058f5 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_data_layer.test.ts @@ -20,6 +20,9 @@ describe('extendedDataLayerConfig', () => { splitAccessor: 'd', xScaleType: 'linear', isHistogram: false, + isHorizontal: false, + isPercentage: false, + isStacked: false, palette: mockPaletteOutput, }; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/grid_lines_config.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/grid_lines_config.test.ts deleted file mode 100644 index b42465da15ef1..0000000000000 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/grid_lines_config.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { AxesSettingsConfig } from '../types'; -import { gridlinesConfigFunction } from '.'; -import { createMockExecutionContext } from '@kbn/expressions-plugin/common/mocks'; - -describe('gridlinesConfig', () => { - test('produces the correct arguments', () => { - const args: AxesSettingsConfig = { x: true, yLeft: false, yRight: false }; - const result = gridlinesConfigFunction.fn(null, args, createMockExecutionContext()); - - expect(result).toEqual({ type: 'gridlinesConfig', ...args }); - }); -}); diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/grid_lines_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/grid_lines_config.ts deleted file mode 100644 index 5010ca39cbdc4..0000000000000 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/grid_lines_config.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; -import { GRID_LINES_CONFIG } from '../constants'; -import { AxesSettingsConfig, GridlinesConfigResult } from '../types'; - -export const gridlinesConfigFunction: ExpressionFunctionDefinition< - typeof GRID_LINES_CONFIG, - null, - AxesSettingsConfig, - GridlinesConfigResult -> = { - name: GRID_LINES_CONFIG, - aliases: [], - type: GRID_LINES_CONFIG, - help: i18n.translate('expressionXY.gridlinesConfig.help', { - defaultMessage: `Configure the xy chart's gridlines appearance`, - }), - inputTypes: ['null'], - args: { - x: { - types: ['boolean'], - help: i18n.translate('expressionXY.gridlinesConfig.x.help', { - defaultMessage: 'Specifies whether or not the gridlines of the x-axis are visible.', - }), - }, - yLeft: { - types: ['boolean'], - help: i18n.translate('expressionXY.gridlinesConfig.yLeft.help', { - defaultMessage: 'Specifies whether or not the gridlines of the left y-axis are visible.', - }), - }, - yRight: { - types: ['boolean'], - help: i18n.translate('expressionXY.gridlinesConfig.yRight.help', { - defaultMessage: 'Specifies whether or not the gridlines of the right y-axis are visible.', - }), - }, - }, - fn(input, args) { - return { - type: GRID_LINES_CONFIG, - ...args, - }; - }, -}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts index dc82220db6e23..f58f59ab7b1a0 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts @@ -11,13 +11,11 @@ export * from './layered_xy_vis'; export * from './legend_config'; export * from './annotation_layer'; export * from './extended_annotation_layer'; +export * from './data_decoration_config'; export * from './y_axis_config'; -export * from './extended_y_axis_config'; +export * from './x_axis_config'; +export * from './reference_line_decoration_config'; export * from './extended_data_layer'; -export * from './grid_lines_config'; export * from './axis_extent_config'; -export * from './tick_labels_config'; -export * from './labels_orientation_config'; export * from './reference_line'; export * from './reference_line_layer'; -export * from './axis_titles_visibility_config'; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/labels_orientation_config.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/labels_orientation_config.test.ts deleted file mode 100644 index c4e795c7f6169..0000000000000 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/labels_orientation_config.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { LabelsOrientationConfig } from '../types'; -import { labelsOrientationConfigFunction } from '.'; -import { createMockExecutionContext } from '@kbn/expressions-plugin/common/mocks'; - -describe('labelsOrientationConfig', () => { - test('produces the correct arguments', () => { - const args: LabelsOrientationConfig = { x: 0, yLeft: -90, yRight: -45 }; - const result = labelsOrientationConfigFunction.fn(null, args, createMockExecutionContext()); - - expect(result).toEqual({ type: 'labelsOrientationConfig', ...args }); - }); -}); diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/labels_orientation_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/labels_orientation_config.ts deleted file mode 100644 index 5143f4a3e25bd..0000000000000 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/labels_orientation_config.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; -import { LABELS_ORIENTATION_CONFIG } from '../constants'; -import { LabelsOrientationConfig, LabelsOrientationConfigResult } from '../types'; - -export const labelsOrientationConfigFunction: ExpressionFunctionDefinition< - typeof LABELS_ORIENTATION_CONFIG, - null, - LabelsOrientationConfig, - LabelsOrientationConfigResult -> = { - name: LABELS_ORIENTATION_CONFIG, - aliases: [], - type: LABELS_ORIENTATION_CONFIG, - help: i18n.translate('expressionXY.labelsOrientationConfig.help', { - defaultMessage: `Configure the xy chart's tick labels orientation`, - }), - inputTypes: ['null'], - args: { - x: { - types: ['number'], - options: [0, -90, -45], - help: i18n.translate('expressionXY.labelsOrientationConfig.x.help', { - defaultMessage: 'Specifies the labels orientation of the x-axis.', - }), - }, - yLeft: { - types: ['number'], - options: [0, -90, -45], - help: i18n.translate('expressionXY.labelsOrientationConfig.yLeft.help', { - defaultMessage: 'Specifies the labels orientation of the left y-axis.', - }), - }, - yRight: { - types: ['number'], - options: [0, -90, -45], - help: i18n.translate('expressionXY.labelsOrientationConfig.yRight.help', { - defaultMessage: 'Specifies the labels orientation of the right y-axis.', - }), - }, - }, - fn(input, args) { - return { - type: LABELS_ORIENTATION_CONFIG, - ...args, - }; - }, -}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts index fb7c91c682847..9e1ef1cc0cb9f 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts @@ -15,6 +15,7 @@ import { validateMinTimeBarInterval, hasBarLayer, errors, + validateAxes, } from './validate'; import { appendLayerIds, getDataLayers } from '../helpers'; @@ -35,6 +36,8 @@ export const layeredXyVisFn: LayeredXyVisFn['fn'] = async (data, args, handlers) throw new Error(errors.markSizeRatioWithoutAccessor()); } + validateAxes(dataLayers, args.yAxisConfigs); + return { type: 'render', as: XY_VIS_RENDERER, diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.test.ts index 4c7c2e3dc628f..7e3230d075fcf 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.test.ts @@ -15,6 +15,7 @@ describe('referenceLine', () => { const args: ReferenceLineArgs = { value: 100, fill: 'above', + position: 'bottom', }; const result = referenceLineFunction.fn(null, args, createMockExecutionContext()); @@ -23,9 +24,9 @@ describe('referenceLine', () => { type: 'referenceLine', layerType: 'referenceLine', lineLength: 0, - yConfig: [ + decorations: [ { - type: 'referenceLineYConfig', + type: 'extendedReferenceLineDecorationConfig', ...args, textVisibility: false, }, @@ -40,7 +41,7 @@ describe('referenceLine', () => { value: 100, icon: 'alert', iconPosition: 'below', - axisMode: 'bottom', + position: 'bottom', lineStyle: 'solid', lineWidth: 10, color: '#fff', @@ -54,9 +55,9 @@ describe('referenceLine', () => { type: 'referenceLine', layerType: 'referenceLine', lineLength: 0, - yConfig: [ + decorations: [ { - type: 'referenceLineYConfig', + type: 'extendedReferenceLineDecorationConfig', ...args, }, ], @@ -69,6 +70,7 @@ describe('referenceLine', () => { name: 'some name', value: 100, fill: 'none', + position: 'bottom', }; const result = referenceLineFunction.fn(null, args, createMockExecutionContext()); @@ -77,9 +79,9 @@ describe('referenceLine', () => { type: 'referenceLine', layerType: 'referenceLine', lineLength: 0, - yConfig: [ + decorations: [ { - type: 'referenceLineYConfig', + type: 'extendedReferenceLineDecorationConfig', ...args, textVisibility: true, }, @@ -93,6 +95,7 @@ describe('referenceLine', () => { value: 100, textVisibility: true, fill: 'none', + position: 'bottom', }; const result = referenceLineFunction.fn(null, args, createMockExecutionContext()); @@ -101,9 +104,9 @@ describe('referenceLine', () => { type: 'referenceLine', layerType: 'referenceLine', lineLength: 0, - yConfig: [ + decorations: [ { - type: 'referenceLineYConfig', + type: 'extendedReferenceLineDecorationConfig', ...args, textVisibility: false, }, @@ -119,6 +122,7 @@ describe('referenceLine', () => { name: 'some text', textVisibility, fill: 'none', + position: 'bottom', }; const result = referenceLineFunction.fn(null, args, createMockExecutionContext()); @@ -127,9 +131,9 @@ describe('referenceLine', () => { type: 'referenceLine', layerType: 'referenceLine', lineLength: 0, - yConfig: [ + decorations: [ { - type: 'referenceLineYConfig', + type: 'extendedReferenceLineDecorationConfig', ...args, textVisibility, }, diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.ts index c294d6ca5aaec..0f3a3a4f6b1fb 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { Position } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import { AvailableReferenceLineIcons, @@ -14,8 +15,7 @@ import { LayerTypes, LineStyles, REFERENCE_LINE, - REFERENCE_LINE_Y_CONFIG, - YAxisModes, + EXTENDED_REFERENCE_LINE_DECORATION_CONFIG, } from '../constants'; import { ReferenceLineFn } from '../types'; import { strings } from '../i18n'; @@ -36,13 +36,23 @@ export const referenceLineFunction: ReferenceLineFn = { help: strings.getReferenceLineValueHelp(), required: true, }, - axisMode: { + position: { types: ['string'], - options: [...Object.values(YAxisModes)], - help: strings.getAxisModeHelp(), - default: YAxisModes.AUTO, + options: [Position.Right, Position.Left], + help: i18n.translate('expressionXY.referenceLine.position.help', { + defaultMessage: + 'Position of axis (first axis of that position) to which the reference line belongs.', + }), + default: Position.Left, strict: true, }, + axisId: { + types: ['string'], + help: i18n.translate('expressionXY.referenceLine.axisId.help', { + defaultMessage: + 'Id of axis to which the reference line belongs. It has higher priority than "position"', + }), + }, color: { types: ['string'], help: strings.getColorHelp(), @@ -50,7 +60,7 @@ export const referenceLineFunction: ReferenceLineFn = { lineStyle: { types: ['string'], options: [...Object.values(LineStyles)], - help: i18n.translate('expressionXY.yConfig.lineStyle.help', { + help: i18n.translate('expressionXY.decorationConfig.lineStyle.help', { defaultMessage: 'The style of the reference line', }), default: LineStyles.SOLID, @@ -58,14 +68,14 @@ export const referenceLineFunction: ReferenceLineFn = { }, lineWidth: { types: ['number'], - help: i18n.translate('expressionXY.yConfig.lineWidth.help', { + help: i18n.translate('expressionXY.decorationConfig.lineWidth.help', { defaultMessage: 'The width of the reference line', }), default: 1, }, icon: { types: ['string'], - help: i18n.translate('expressionXY.yConfig.icon.help', { + help: i18n.translate('expressionXY.decorationConfig.icon.help', { defaultMessage: 'An optional icon used for reference lines', }), options: [...Object.values(AvailableReferenceLineIcons)], @@ -74,7 +84,7 @@ export const referenceLineFunction: ReferenceLineFn = { iconPosition: { types: ['string'], options: [...Object.values(IconPositions)], - help: i18n.translate('expressionXY.yConfig.iconPosition.help', { + help: i18n.translate('expressionXY.decorationConfig.iconPosition.help', { defaultMessage: 'The placement of the icon for the reference line', }), default: IconPositions.AUTO, @@ -82,14 +92,14 @@ export const referenceLineFunction: ReferenceLineFn = { }, textVisibility: { types: ['boolean'], - help: i18n.translate('expressionXY.yConfig.textVisibility.help', { + help: i18n.translate('expressionXY.decorationConfig.textVisibility.help', { defaultMessage: 'Visibility of the label on the reference line', }), }, fill: { types: ['string'], options: [...Object.values(FillStyles)], - help: i18n.translate('expressionXY.yConfig.fill.help', { + help: i18n.translate('expressionXY.decorationConfig.fill.help', { defaultMessage: 'Fill', }), default: FillStyles.NONE, @@ -108,7 +118,7 @@ export const referenceLineFunction: ReferenceLineFn = { type: REFERENCE_LINE, layerType: LayerTypes.REFERENCELINE, lineLength: table?.rows.length ?? 0, - yConfig: [{ ...args, textVisibility, type: REFERENCE_LINE_Y_CONFIG }], + decorations: [{ ...args, textVisibility, type: EXTENDED_REFERENCE_LINE_DECORATION_CONFIG }], }; }, }; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_y_axis_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_decoration_config.ts similarity index 55% rename from src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_y_axis_config.ts rename to src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_decoration_config.ts index 606cdd84ac710..ad25d7e0c3226 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/extended_y_axis_config.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_decoration_config.ts @@ -6,43 +6,54 @@ * Side Public License, v 1. */ +import { Position } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import { AvailableReferenceLineIcons, - EXTENDED_Y_CONFIG, + REFERENCE_LINE_DECORATION_CONFIG, FillStyles, IconPositions, LineStyles, } from '../constants'; import { strings } from '../i18n'; -import { ExtendedYConfigFn } from '../types'; -import { commonYConfigArgs } from './common_y_config_args'; +import { ReferenceLineDecorationConfigFn } from '../types'; +import { commonDecorationConfigArgs } from './common_y_config_args'; -export const extendedYAxisConfigFunction: ExtendedYConfigFn = { - name: EXTENDED_Y_CONFIG, +export const referenceLineDecorationConfigFunction: ReferenceLineDecorationConfigFn = { + name: REFERENCE_LINE_DECORATION_CONFIG, aliases: [], - type: EXTENDED_Y_CONFIG, - help: strings.getYConfigFnHelp(), + type: REFERENCE_LINE_DECORATION_CONFIG, + help: strings.getDecorationsHelp(), inputTypes: ['null'], args: { - ...commonYConfigArgs, + ...commonDecorationConfigArgs, + position: { + types: ['string'], + options: [Position.Right, Position.Left, Position.Bottom], + help: i18n.translate('expressionXY.referenceLine.position.help', { + defaultMessage: + 'Position of axis (first axis of that position) to which the reference line belongs.', + }), + default: Position.Left, + strict: true, + }, lineStyle: { types: ['string'], options: [...Object.values(LineStyles)], - help: i18n.translate('expressionXY.yConfig.lineStyle.help', { + help: i18n.translate('expressionXY.decorationConfig.lineStyle.help', { defaultMessage: 'The style of the reference line', }), strict: true, }, lineWidth: { types: ['number'], - help: i18n.translate('expressionXY.yConfig.lineWidth.help', { + help: i18n.translate('expressionXY.decorationConfig.lineWidth.help', { defaultMessage: 'The width of the reference line', }), }, icon: { types: ['string'], - help: i18n.translate('expressionXY.yConfig.icon.help', { + help: i18n.translate('expressionXY.decorationConfig.icon.help', { defaultMessage: 'An optional icon used for reference lines', }), options: [...Object.values(AvailableReferenceLineIcons)], @@ -51,21 +62,21 @@ export const extendedYAxisConfigFunction: ExtendedYConfigFn = { iconPosition: { types: ['string'], options: [...Object.values(IconPositions)], - help: i18n.translate('expressionXY.yConfig.iconPosition.help', { + help: i18n.translate('expressionXY.decorationConfig.iconPosition.help', { defaultMessage: 'The placement of the icon for the reference line', }), strict: true, }, textVisibility: { types: ['boolean'], - help: i18n.translate('expressionXY.yConfig.textVisibility.help', { + help: i18n.translate('expressionXY.decorationConfig.textVisibility.help', { defaultMessage: 'Visibility of the label on the reference line', }), }, fill: { types: ['string'], options: [...Object.values(FillStyles)], - help: i18n.translate('expressionXY.yConfig.fill.help', { + help: i18n.translate('expressionXY.decorationConfig.fill.help', { defaultMessage: 'Fill', }), strict: true, @@ -73,7 +84,7 @@ export const extendedYAxisConfigFunction: ExtendedYConfigFn = { }, fn(input, args) { return { - type: EXTENDED_Y_CONFIG, + type: REFERENCE_LINE_DECORATION_CONFIG, ...args, }; }, diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts index 234001015d73a..d797acc647f0d 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/reference_line_layer.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { REFERENCE_LINE_LAYER, EXTENDED_Y_CONFIG } from '../constants'; +import { REFERENCE_LINE_LAYER, REFERENCE_LINE_DECORATION_CONFIG } from '../constants'; import { ReferenceLineLayerFn } from '../types'; import { strings } from '../i18n'; @@ -22,9 +22,9 @@ export const referenceLineLayerFunction: ReferenceLineLayerFn = { help: strings.getRLAccessorsHelp(), multi: true, }, - yConfig: { - types: [EXTENDED_Y_CONFIG], - help: strings.getRLYConfigHelp(), + decorations: { + types: [REFERENCE_LINE_DECORATION_CONFIG], + help: strings.getRLDecorationConfigHelp(), multi: true, }, columnToLabel: { diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/tick_labels_config.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/tick_labels_config.test.ts deleted file mode 100644 index d856af48f5d88..0000000000000 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/tick_labels_config.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { AxesSettingsConfig } from '../types'; -import { tickLabelsConfigFunction } from '.'; -import { createMockExecutionContext } from '@kbn/expressions-plugin/common/mocks'; - -describe('tickLabelsConfig', () => { - test('produces the correct arguments', () => { - const args: AxesSettingsConfig = { x: true, yLeft: false, yRight: false }; - const result = tickLabelsConfigFunction.fn(null, args, createMockExecutionContext()); - - expect(result).toEqual({ type: 'tickLabelsConfig', ...args }); - }); -}); diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/tick_labels_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/tick_labels_config.ts deleted file mode 100644 index 4ee40054d6271..0000000000000 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/tick_labels_config.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; -import { TICK_LABELS_CONFIG } from '../constants'; -import { AxesSettingsConfig, TickLabelsConfigResult } from '../types'; - -export const tickLabelsConfigFunction: ExpressionFunctionDefinition< - typeof TICK_LABELS_CONFIG, - null, - AxesSettingsConfig, - TickLabelsConfigResult -> = { - name: TICK_LABELS_CONFIG, - aliases: [], - type: TICK_LABELS_CONFIG, - help: i18n.translate('expressionXY.tickLabelsConfig.help', { - defaultMessage: `Configure the xy chart's tick labels appearance`, - }), - inputTypes: ['null'], - args: { - x: { - types: ['boolean'], - help: i18n.translate('expressionXY.tickLabelsConfig.x.help', { - defaultMessage: 'Specifies whether or not the tick labels of the x-axis are visible.', - }), - }, - yLeft: { - types: ['boolean'], - help: i18n.translate('expressionXY.tickLabelsConfig.yLeft.help', { - defaultMessage: 'Specifies whether or not the tick labels of the left y-axis are visible.', - }), - }, - yRight: { - types: ['boolean'], - help: i18n.translate('expressionXY.tickLabelsConfig.yRight.help', { - defaultMessage: 'Specifies whether or not the tick labels of the right y-axis are visible.', - }), - }, - }, - fn(input, args) { - return { - type: TICK_LABELS_CONFIG, - ...args, - }; - }, -}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts index 6cf8f4c68d1db..9b8183abfa205 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { isValidInterval } from '@kbn/data-plugin/common'; import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; -import { AxisExtentModes, ValueLabelModes } from '../constants'; +import { AxisExtentModes, ValueLabelModes, SeriesTypes } from '../constants'; import { SeriesType, AxisExtentConfigResult, @@ -17,7 +17,9 @@ import { CommonXYDataLayerConfigResult, ValueLabelMode, CommonXYDataLayerConfig, + YAxisConfigResult, ExtendedDataLayerConfigResult, + XAxisConfigResult, } from '../types'; import { isTimeChart } from '../helpers'; @@ -111,19 +113,23 @@ export const errors = { defaultMessage: '`minTimeBarInterval` argument is applicable only for time bar charts.', } ), + axisIsNotAssignedError: (axisId: string) => + i18n.translate('expressionXY.reusable.function.xyVis.errors.axisIsNotAssignedError', { + defaultMessage: + 'Axis with id: "{axisId}" is not assigned to any accessor. Please assign axis using the following construction: `decorations=\\{dataDecorationConfig forAccessor="your-accessor" axisId="{axisId}"\\}`', + values: { axisId }, + }), }; export const hasBarLayer = (layers: Array) => - layers.filter(({ seriesType }) => seriesType.includes('bar')).length > 0; + layers.some(({ seriesType }) => seriesType === SeriesTypes.BAR); export const hasAreaLayer = (layers: Array) => - layers.filter(({ seriesType }) => seriesType.includes('area')).length > 0; + layers.some(({ seriesType }) => seriesType === SeriesTypes.AREA); export const hasHistogramBarLayer = ( layers: Array -) => - layers.filter(({ seriesType, isHistogram }) => seriesType.includes('bar') && isHistogram).length > - 0; +) => layers.some(({ seriesType, isHistogram }) => seriesType === SeriesTypes.BAR && isHistogram); export const isValidExtentWithCustomMode = (extent: AxisExtentConfigResult) => { const isValidLowerBound = @@ -138,8 +144,8 @@ export const validateExtentForDataBounds = ( extent: AxisExtentConfigResult, layers: Array ) => { - const lineSeries = layers.filter(({ seriesType }) => seriesType.includes('line')); - if (!lineSeries.length && extent.mode === AxisExtentModes.DATA_BOUNDS) { + const hasLineSeries = layers.some(({ seriesType }) => seriesType === SeriesTypes.LINE); + if (!hasLineSeries && extent.mode === AxisExtentModes.DATA_BOUNDS) { throw new Error(errors.dataBoundsForNotLineChartError()); } }; @@ -158,20 +164,46 @@ export const validateXExtent = ( } }; -export const validateExtent = ( - extent: AxisExtentConfigResult, +export const validateExtents = ( + dataLayers: Array, hasBarOrArea: boolean, - dataLayers: Array + yAxisConfigs?: YAxisConfigResult[], + xAxisConfig?: XAxisConfigResult ) => { - if ( - extent.mode === AxisExtentModes.CUSTOM && - hasBarOrArea && - !isValidExtentWithCustomMode(extent) - ) { - throw new Error(errors.extendBoundsAreInvalidError()); - } + yAxisConfigs?.forEach((axis) => { + if (!axis.extent) { + return; + } + if ( + hasBarOrArea && + axis.extent?.mode === AxisExtentModes.CUSTOM && + !isValidExtentWithCustomMode(axis.extent) + ) { + throw new Error(errors.extendBoundsAreInvalidError()); + } + + validateExtentForDataBounds(axis.extent, dataLayers); + }); + + validateXExtent(xAxisConfig?.extent, dataLayers); +}; - validateExtentForDataBounds(extent, dataLayers); +export const validateAxes = ( + dataLayers: Array, + yAxisConfigs?: YAxisConfigResult[] +) => { + yAxisConfigs?.forEach((axis) => { + if ( + axis.id && + dataLayers.every( + (layer) => + !layer.decorations || + layer.decorations?.every((decorationConfig) => decorationConfig.axisId !== axis.id) + ) + ) { + throw new Error(errors.axisIsNotAssignedError(axis.id)); + } + }); }; export const validateFillOpacity = (fillOpacity: number | undefined, hasArea: boolean) => { @@ -191,7 +223,7 @@ export const validateValueLabels = ( }; const isAreaOrLineChart = (seriesType: SeriesType) => - seriesType.includes('line') || seriesType.includes('area'); + seriesType === SeriesTypes.LINE || seriesType === SeriesTypes.AREA; export const validateAddTimeMarker = ( dataLayers: Array, @@ -206,7 +238,7 @@ export const validateMarkSizeForChartType = ( markSizeAccessor: ExpressionValueVisDimension | string | undefined, seriesType: SeriesType ) => { - if (markSizeAccessor && !seriesType.includes('line') && !seriesType.includes('area')) { + if (markSizeAccessor && !isAreaOrLineChart(seriesType)) { throw new Error(errors.markSizeAccessorForNonLineOrAreaChartsError()); } }; @@ -248,7 +280,7 @@ export const validateLinesVisibilityForChartType = ( showLines: boolean | undefined, seriesType: SeriesType ) => { - if (showLines && !(seriesType.includes('line') || seriesType.includes('area'))) { + if (showLines && !isAreaOrLineChart(seriesType)) { throw new Error(errors.linesVisibilityForNonLineChartError()); } }; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/x_axis_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/x_axis_config.ts new file mode 100644 index 0000000000000..cf7309cfa56bf --- /dev/null +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/x_axis_config.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Position } from '@elastic/charts'; +import { strings } from '../i18n'; +import { XAxisConfigFn } from '../types'; +import { X_AXIS_CONFIG } from '../constants'; +import { commonAxisConfigArgs } from './common_axis_args'; + +export const xAxisConfigFunction: XAxisConfigFn = { + name: X_AXIS_CONFIG, + aliases: [], + type: X_AXIS_CONFIG, + help: strings.getXAxisConfigFnHelp(), + inputTypes: ['null'], + args: { + ...commonAxisConfigArgs, + position: { + types: ['string'], + options: [Position.Top, Position.Bottom], + help: strings.getAxisPositionHelp(), + strict: true, + }, + }, + fn(input, args) { + return { + type: X_AXIS_CONFIG, + ...args, + position: args.position ?? Position.Bottom, + }; + }, +}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts index bbd906b724844..0b81682eb4381 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts @@ -233,7 +233,10 @@ describe('xyVis', () => { annotationLayers: [], isHistogram: true, xScaleType: 'time', - xExtent: { type: 'axisExtentConfig', mode: 'dataBounds' }, + xAxisConfig: { + type: 'xAxisConfig', + extent: { type: 'axisExtentConfig', mode: 'dataBounds' }, + }, }, createMockExecutionContext() ) @@ -255,11 +258,14 @@ describe('xyVis', () => { ...restLayerArgs, referenceLines: [], annotationLayers: [], - xExtent: { - type: 'axisExtentConfig', - mode: 'full', - lowerBound: undefined, - upperBound: undefined, + xAxisConfig: { + type: 'xAxisConfig', + extent: { + type: 'axisExtentConfig', + mode: 'full', + lowerBound: undefined, + upperBound: undefined, + }, }, }, createMockExecutionContext() @@ -282,9 +288,9 @@ describe('xyVis', () => { ...restLayerArgs, referenceLines: [], annotationLayers: [], - xExtent: { - type: 'axisExtentConfig', - mode: 'dataBounds', + xAxisConfig: { + type: 'xAxisConfig', + extent: { type: 'axisExtentConfig', mode: 'dataBounds' }, }, }, createMockExecutionContext() @@ -292,7 +298,7 @@ describe('xyVis', () => { ).rejects.toThrowErrorMatchingSnapshot(); }); - test('it renders with custom xExtent for a numeric histogram', async () => { + test('it renders with custom x-axis extent for a numeric histogram', async () => { const { data, args } = sampleArgs(); const { layers, ...rest } = args; const { layerId, layerType, table, type, ...restLayerArgs } = sampleLayer; @@ -304,11 +310,14 @@ describe('xyVis', () => { referenceLines: [], annotationLayers: [], isHistogram: true, - xExtent: { - type: 'axisExtentConfig', - mode: 'custom', - lowerBound: 0, - upperBound: 10, + xAxisConfig: { + type: 'xAxisConfig', + extent: { + type: 'axisExtentConfig', + mode: 'custom', + lowerBound: 0, + upperBound: 10, + }, }, }, createMockExecutionContext() @@ -320,11 +329,14 @@ describe('xyVis', () => { value: { args: { ...rest, - xExtent: { - type: 'axisExtentConfig', - mode: 'custom', - lowerBound: 0, - upperBound: 10, + xAxisConfig: { + type: 'xAxisConfig', + extent: { + type: 'axisExtentConfig', + mode: 'custom', + lowerBound: 0, + upperBound: 10, + }, }, layers: [ { diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts index c24f28f5ca173..00c29436926f4 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts @@ -21,7 +21,7 @@ import { hasAreaLayer, hasBarLayer, hasHistogramBarLayer, - validateExtent, + validateExtents, validateFillOpacity, validateMarkSizeRatioLimits, validateValueLabels, @@ -33,7 +33,7 @@ import { validateLineWidthForChartType, validatePointsRadiusForChartType, validateLinesVisibilityForChartType, - validateXExtent, + validateAxes, } from './validate'; const createDataLayer = (args: XYArgs, table: Datatable): DataLayerConfigResult => { @@ -46,8 +46,11 @@ const createDataLayer = (args: XYArgs, table: Datatable): DataLayerConfigResult columnToLabel: args.columnToLabel, xScaleType: args.xScaleType, isHistogram: args.isHistogram, + isPercentage: args.isPercentage, + isHorizontal: args.isHorizontal, + isStacked: args.isStacked, palette: args.palette, - yConfig: args.yConfig, + decorations: args.decorations, showPoints: args.showPoints, pointsRadius: args.pointsRadius, lineWidth: args.lineWidth, @@ -74,7 +77,10 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { columnToLabel, xScaleType, isHistogram, - yConfig, + isHorizontal, + isPercentage, + isStacked, + decorations, palette, markSizeAccessor, showPoints, @@ -121,9 +127,7 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { const hasBar = hasBarLayer(dataLayers); const hasArea = hasAreaLayer(dataLayers); - validateXExtent(args.xExtent, dataLayers); - validateExtent(args.yLeftExtent, hasBar || hasArea, dataLayers); - validateExtent(args.yRightExtent, hasBar || hasArea, dataLayers); + validateExtents(dataLayers, hasBar || hasArea, args.yAxisConfigs, args.xAxisConfig); validateFillOpacity(args.fillOpacity, hasArea); validateAddTimeMarker(dataLayers, args.addTimeMarker); validateMinTimeBarInterval(dataLayers, hasBar, args.minTimeBarInterval); @@ -136,6 +140,7 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { validateLineWidthForChartType(lineWidth, args.seriesType); validateShowPointsForChartType(showPoints, args.seriesType); validatePointsRadiusForChartType(pointsRadius, args.seriesType); + validateAxes(dataLayers, args.yAxisConfigs); return { type: 'render', diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/y_axis_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/y_axis_config.ts index 882a3231148f5..64f58dc5acb98 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/y_axis_config.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/y_axis_config.ts @@ -6,22 +6,46 @@ * Side Public License, v 1. */ -import { Y_CONFIG } from '../constants'; -import { YConfigFn } from '../types'; +import { Position } from '@elastic/charts'; import { strings } from '../i18n'; -import { commonYConfigArgs } from './common_y_config_args'; +import { Y_AXIS_CONFIG, AxisModes, YScaleTypes } from '../constants'; +import { YAxisConfigFn } from '../types'; +import { commonAxisConfigArgs } from './common_axis_args'; -export const yAxisConfigFunction: YConfigFn = { - name: Y_CONFIG, +export const yAxisConfigFunction: YAxisConfigFn = { + name: Y_AXIS_CONFIG, aliases: [], - type: Y_CONFIG, - help: strings.getYConfigFnHelp(), + type: Y_AXIS_CONFIG, + help: strings.getYAxisConfigFnHelp(), inputTypes: ['null'], - args: { ...commonYConfigArgs }, + args: { + ...commonAxisConfigArgs, + mode: { + types: ['string'], + options: [...Object.values(AxisModes)], + help: strings.getAxisModeHelp(), + }, + boundsMargin: { + types: ['number'], + help: strings.getAxisBoundsMarginHelp(), + }, + scaleType: { + options: [...Object.values(YScaleTypes)], + help: strings.getAxisScaleTypeHelp(), + default: YScaleTypes.LINEAR, + }, + position: { + types: ['string'], + options: [Position.Right, Position.Left], + help: strings.getAxisPositionHelp(), + strict: true, + }, + }, fn(input, args) { return { - type: Y_CONFIG, + type: Y_AXIS_CONFIG, ...args, + position: args.position ?? Position.Left, }; }, }; diff --git a/src/plugins/chart_expressions/expression_xy/common/helpers/layers.test.ts b/src/plugins/chart_expressions/expression_xy/common/helpers/layers.test.ts index 895abdb7a60df..d821f2aa8d147 100644 --- a/src/plugins/chart_expressions/expression_xy/common/helpers/layers.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/helpers/layers.test.ts @@ -59,6 +59,9 @@ describe('#getDataLayers', () => { seriesType: 'bar', xScaleType: 'time', isHistogram: false, + isHorizontal: false, + isPercentage: false, + isStacked: false, table: { rows: [], columns: [], type: 'datatable' }, palette: { type: 'system_palette', name: 'system' }, }, diff --git a/src/plugins/chart_expressions/expression_xy/common/helpers/layers.ts b/src/plugins/chart_expressions/expression_xy/common/helpers/layers.ts index d538db2d3c496..83ea26eea262a 100644 --- a/src/plugins/chart_expressions/expression_xy/common/helpers/layers.ts +++ b/src/plugins/chart_expressions/expression_xy/common/helpers/layers.ts @@ -14,7 +14,7 @@ import { ExtendedDataLayerArgs, DataLayerArgs, } from '../types'; -import { LayerTypes } from '../constants'; +import { LayerTypes, SeriesTypes } from '../constants'; function isWithLayerId(layer: T): layer is T & WithLayerId { return (layer as T & WithLayerId).layerId ? true : false; @@ -35,9 +35,7 @@ export function appendLayerIds( } export const getShowLines = (args: DataLayerArgs | ExtendedDataLayerArgs) => - args.seriesType.includes('line') || args.seriesType.includes('area') - ? args.showLines ?? true - : args.showLines; + args.showLines ?? (args.seriesType === SeriesTypes.LINE || args.seriesType !== SeriesTypes.AREA); export function getDataLayers(layers: XYExtendedLayerConfigResult[]) { return layers.filter( diff --git a/src/plugins/chart_expressions/expression_xy/common/helpers/visualization.test.ts b/src/plugins/chart_expressions/expression_xy/common/helpers/visualization.test.ts index 678c342e38b49..14903c0c54a27 100644 --- a/src/plugins/chart_expressions/expression_xy/common/helpers/visualization.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/helpers/visualization.test.ts @@ -20,6 +20,9 @@ describe('#isTimeChart', () => { seriesType: 'bar', xScaleType: 'time', isHistogram: false, + isHorizontal: false, + isPercentage: false, + isStacked: false, table: { rows: [], columns: [ diff --git a/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx index def7a6170232c..9d1388829de3c 100644 --- a/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx +++ b/src/plugins/chart_expressions/expression_xy/common/i18n/index.tsx @@ -29,38 +29,6 @@ export const strings = { i18n.translate('expressionXY.xyVis.logDatatable.breakDown', { defaultMessage: 'Break down by', }), - getXTitleHelp: () => - i18n.translate('expressionXY.xyVis.xTitle.help', { - defaultMessage: 'X axis title', - }), - getYTitleHelp: () => - i18n.translate('expressionXY.xyVis.yLeftTitle.help', { - defaultMessage: 'Y left axis title', - }), - getYRightTitleHelp: () => - i18n.translate('expressionXY.xyVis.yRightTitle.help', { - defaultMessage: 'Y right axis title', - }), - getXExtentHelp: () => - i18n.translate('expressionXY.xyVis.xExtent.help', { - defaultMessage: 'X axis extents', - }), - getYLeftExtentHelp: () => - i18n.translate('expressionXY.xyVis.yLeftExtent.help', { - defaultMessage: 'Y left axis extents', - }), - getYRightExtentHelp: () => - i18n.translate('expressionXY.xyVis.yRightExtent.help', { - defaultMessage: 'Y right axis extents', - }), - getYLeftScaleTypeHelp: () => - i18n.translate('expressionXY.xyVis.yLeftScaleType.help', { - defaultMessage: 'The scale type of the left y axis', - }), - getYRightScaleTypeHelp: () => - i18n.translate('expressionXY.xyVis.yRightScaleType.help', { - defaultMessage: 'The scale type of the right y axis', - }), getLegendHelp: () => i18n.translate('expressionXY.xyVis.legend.help', { defaultMessage: 'Configure the chart legend.', @@ -77,22 +45,6 @@ export const strings = { i18n.translate('expressionXY.xyVis.valueLabels.help', { defaultMessage: 'Value labels mode', }), - getTickLabelsVisibilitySettingsHelp: () => - i18n.translate('expressionXY.xyVis.tickLabelsVisibilitySettings.help', { - defaultMessage: 'Show x and y axes tick labels', - }), - getLabelsOrientationHelp: () => - i18n.translate('expressionXY.xyVis.labelsOrientation.help', { - defaultMessage: 'Defines the rotation of the axis labels', - }), - getGridlinesVisibilitySettingsHelp: () => - i18n.translate('expressionXY.xyVis.gridlinesVisibilitySettings.help', { - defaultMessage: 'Show x and y axes gridlines', - }), - getAxisTitlesVisibilitySettingsHelp: () => - i18n.translate('expressionXY.xyVis.axisTitlesVisibilitySettings.help', { - defaultMessage: 'Show x and y axes titles', - }), getDataLayerHelp: () => i18n.translate('expressionXY.xyVis.dataLayer.help', { defaultMessage: 'Data layer of visual series', @@ -125,6 +77,14 @@ export const strings = { i18n.translate('expressionXY.xyVis.ariaLabel.help', { defaultMessage: 'Specifies the aria label of the xy chart', }), + getXAxisConfigHelp: () => + i18n.translate('expressionXY.xyVis.xAxisConfig.help', { + defaultMessage: 'Specifies x-axis config', + }), + getyAxisConfigsHelp: () => + i18n.translate('expressionXY.xyVis.yAxisConfigs.help', { + defaultMessage: 'Specifies y-axes configs', + }), getDetailedTooltipHelp: () => i18n.translate('expressionXY.xyVis.detailedTooltip.help', { defaultMessage: 'Show detailed tooltip', @@ -185,6 +145,18 @@ export const strings = { i18n.translate('expressionXY.dataLayer.isHistogram.help', { defaultMessage: 'Whether to layout the chart as a histogram', }), + getIsStackedHelp: () => + i18n.translate('expressionXY.dataLayer.isStacked.help', { + defaultMessage: 'Layout of the chart in stacked mode', + }), + getIsPercentageHelp: () => + i18n.translate('expressionXY.dataLayer.isPercentage.help', { + defaultMessage: 'Whether to layout the chart has percentage mode', + }), + getIsHorizontalHelp: () => + i18n.translate('expressionXY.dataLayer.isHorizontal.help', { + defaultMessage: 'Layout of the chart is horizontal', + }), getSplitAccessorHelp: () => i18n.translate('expressionXY.dataLayer.splitAccessor.help', { defaultMessage: 'The column to split by', @@ -213,9 +185,9 @@ export const strings = { i18n.translate('expressionXY.dataLayer.showLines.help', { defaultMessage: 'Show lines between points', }), - getYConfigHelp: () => - i18n.translate('expressionXY.dataLayer.yConfig.help', { - defaultMessage: 'Additional configuration for y axes', + getDecorationsHelp: () => + i18n.translate('expressionXY.dataLayer.decorations.help', { + defaultMessage: 'Additional decoration for data', }), getColumnToLabelHelp: () => i18n.translate('expressionXY.layer.columnToLabel.help', { @@ -237,30 +209,26 @@ export const strings = { i18n.translate('expressionXY.referenceLineLayer.accessors.help', { defaultMessage: 'The columns to display on the y axis.', }), - getRLYConfigHelp: () => - i18n.translate('expressionXY.referenceLineLayer.yConfig.help', { - defaultMessage: 'Additional configuration for y axes', + getRLDecorationConfigHelp: () => + i18n.translate('expressionXY.referenceLineLayer.decorationConfig.help', { + defaultMessage: 'Additional decoration for reference line', }), getRLHelp: () => i18n.translate('expressionXY.referenceLineLayer.help', { defaultMessage: `Configure a reference line in the xy chart`, }), - getYConfigFnHelp: () => - i18n.translate('expressionXY.yConfig.help', { - defaultMessage: `Configure the behavior of a xy chart's y axis metric`, - }), getForAccessorHelp: () => - i18n.translate('expressionXY.yConfig.forAccessor.help', { + i18n.translate('expressionXY.decorationConfig.forAccessor.help', { defaultMessage: 'The accessor this configuration is for', }), - getAxisModeHelp: () => - i18n.translate('expressionXY.yConfig.axisMode.help', { - defaultMessage: 'The axis mode of the metric', - }), getColorHelp: () => - i18n.translate('expressionXY.yConfig.color.help', { + i18n.translate('expressionXY.decorationConfig.color.help', { defaultMessage: 'The color of the series', }), + getAxisIdHelp: () => + i18n.translate('expressionXY.decorationConfig.axisId.help', { + defaultMessage: 'Id of axis', + }), getAnnotationLayerFnHelp: () => i18n.translate('expressionXY.annotationLayer.help', { defaultMessage: `Configure an annotation layer in the xy chart`, @@ -273,6 +241,74 @@ export const strings = { i18n.translate('expressionXY.annotationLayer.annotations.help', { defaultMessage: 'Annotations', }), + getXAxisConfigFnHelp: () => + i18n.translate('expressionXY.xAxisConfigFn.help', { + defaultMessage: `Configure the xy chart's x-axis config`, + }), + getYAxisConfigFnHelp: () => + i18n.translate('expressionXY.yAxisConfigFn.help', { + defaultMessage: `Configure the xy chart's y-axis config`, + }), + getAxisModeHelp: () => + i18n.translate('expressionXY.axisConfig.mode.help', { + defaultMessage: 'Scale mode. Can be normal, percentage, wiggle or silhouette', + }), + getAxisBoundsMarginHelp: () => + i18n.translate('expressionXY.axisConfig.boundsMargin.help', { + defaultMessage: 'Margin of bounds', + }), + getAxisExtentHelp: () => + i18n.translate('expressionXY.axisConfig.extent.help', { + defaultMessage: 'Axis extents', + }), + getAxisScaleTypeHelp: () => + i18n.translate('expressionXY.axisConfig.scaleType.help', { + defaultMessage: 'The scale type of the axis', + }), + getAxisTitleHelp: () => + i18n.translate('expressionXY.axisConfig.title.help', { + defaultMessage: 'Title of axis', + }), + getAxisPositionHelp: () => + i18n.translate('expressionXY.axisConfig.position.help', { + defaultMessage: 'Position of axis', + }), + getAxisHideHelp: () => + i18n.translate('expressionXY.axisConfig.hide.help', { + defaultMessage: 'Hide the axis', + }), + getAxisLabelColorHelp: () => + i18n.translate('expressionXY.axisConfig.labelColor.help', { + defaultMessage: 'Color of the axis labels', + }), + getAxisShowOverlappingLabelsHelp: () => + i18n.translate('expressionXY.axisConfig.showOverlappingLabels.help', { + defaultMessage: 'Show overlapping labels', + }), + getAxisShowDuplicatesHelp: () => + i18n.translate('expressionXY.axisConfig.showDuplicates.help', { + defaultMessage: 'Show duplicated ticks', + }), + getAxisShowGridLinesHelp: () => + i18n.translate('expressionXY.axisConfig.showGridLines.help', { + defaultMessage: 'Specifies whether or not the gridlines of the axis are visible', + }), + getAxisLabelsOrientationHelp: () => + i18n.translate('expressionXY.axisConfig.labelsOrientation.help', { + defaultMessage: 'Specifies the labels orientation of the axis', + }), + getAxisShowLabelsHelp: () => + i18n.translate('expressionXY.axisConfig.showLabels.help', { + defaultMessage: 'Show labels', + }), + getAxisShowTitleHelp: () => + i18n.translate('expressionXY.axisConfig.showTitle.help', { + defaultMessage: 'Show title of the axis', + }), + getAxisTruncateHelp: () => + i18n.translate('expressionXY.axisConfig.truncate.help', { + defaultMessage: 'The number of symbols before truncating', + }), getReferenceLineNameHelp: () => i18n.translate('expressionXY.referenceLine.name.help', { defaultMessage: 'Reference line name', diff --git a/src/plugins/chart_expressions/expression_xy/common/index.ts b/src/plugins/chart_expressions/expression_xy/common/index.ts index 005f6c2867c18..d9c415f488839 100755 --- a/src/plugins/chart_expressions/expression_xy/common/index.ts +++ b/src/plugins/chart_expressions/expression_xy/common/index.ts @@ -11,17 +11,17 @@ export const PLUGIN_NAME = 'expressionXy'; export type { XYArgs, - YConfig, EndValue, XYRender, LayerType, - YAxisMode, LineStyle, FillStyle, SeriesType, YScaleType, XScaleType, + AxisMode, AxisConfig, + YAxisConfig, ValidLayer, XYLayerArgs, XYCurveType, @@ -33,30 +33,28 @@ export type { AxisExtentMode, DataLayerConfig, FittingFunction, - ExtendedYConfig, AxisExtentConfig, CollectiveConfig, LegendConfigResult, AxesSettingsConfig, + XAxisConfigResult, + YAxisConfigResult, CommonXYLayerConfig, + DataDecorationConfig, AnnotationLayerArgs, - ExtendedYConfigResult, - GridlinesConfigResult, DataLayerConfigResult, - TickLabelsConfigResult, AxisExtentConfigResult, ReferenceLineLayerArgs, CommonXYDataLayerConfig, - LabelsOrientationConfig, ReferenceLineLayerConfig, AvailableReferenceLineIcon, XYExtendedLayerConfigResult, CommonXYAnnotationLayerConfig, ExtendedDataLayerConfigResult, - LabelsOrientationConfigResult, CommonXYDataLayerConfigResult, ReferenceLineLayerConfigResult, + ReferenceLineDecorationConfig, CommonXYReferenceLineLayerConfig, - AxisTitlesVisibilityConfigResult, + ReferenceLineDecorationConfigResult, CommonXYReferenceLineLayerConfigResult, } from './types'; diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts index 50e0439f757c0..b27863efa4fe8 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts @@ -25,14 +25,11 @@ import { ValueLabelModes, XScaleTypes, XYCurveTypes, - YAxisModes, YScaleTypes, + AxisModes, REFERENCE_LINE, - Y_CONFIG, - AXIS_TITLES_VISIBILITY_CONFIG, - LABELS_ORIENTATION_CONFIG, - TICK_LABELS_CONFIG, - GRID_LINES_CONFIG, + DATA_DECORATION_CONFIG, + REFERENCE_LINE_DECORATION_CONFIG, LEGEND_CONFIG, DATA_LAYER, AXIS_EXTENT_CONFIG, @@ -40,23 +37,24 @@ import { REFERENCE_LINE_LAYER, ANNOTATION_LAYER, EndValues, - EXTENDED_Y_CONFIG, + X_AXIS_CONFIG, + Y_AXIS_CONFIG, AvailableReferenceLineIcons, XY_VIS, LAYERED_XY_VIS, EXTENDED_ANNOTATION_LAYER, - REFERENCE_LINE_Y_CONFIG, + EXTENDED_REFERENCE_LINE_DECORATION_CONFIG, } from '../constants'; import { XYRender } from './expression_renderers'; export type EndValue = $Values; export type LayerType = $Values; -export type YAxisMode = $Values; export type LineStyle = $Values; export type FillStyle = $Values; export type SeriesType = $Values; export type YScaleType = $Values; export type XScaleType = $Values; +export type AxisMode = $Values; export type XYCurveType = $Values; export type IconPosition = $Values; export type ValueLabelMode = $Values; @@ -65,7 +63,6 @@ export type FittingFunction = $Values; export type AvailableReferenceLineIcon = $Values; export interface AxesSettingsConfig { - x: boolean; yLeft: boolean; yRight: boolean; } @@ -77,23 +74,41 @@ export interface AxisExtentConfig { } export interface AxisConfig { - title: string; + title?: string; hide?: boolean; + id?: string; + position?: Position; + labelColor?: string; + showOverlappingLabels?: boolean; + showDuplicates?: boolean; + labelsOrientation?: number; + truncate?: number; + showLabels?: boolean; + showTitle?: boolean; + showGridLines?: boolean; + extent?: AxisExtentConfigResult; } -export interface ExtendedYConfig extends YConfig { +export interface YAxisConfig extends AxisConfig { + mode?: AxisMode; + boundsMargin?: number; + scaleType?: YScaleType; +} + +export interface ReferenceLineDecorationConfig extends DataDecorationConfig { icon?: AvailableReferenceLineIcon; lineWidth?: number; lineStyle?: LineStyle; fill?: FillStyle; iconPosition?: IconPosition; textVisibility?: boolean; + position?: Position; } -export interface YConfig { +export interface DataDecorationConfig { forAccessor: string; - axisMode?: YAxisMode; color?: string; + axisId?: string; } export interface DataLayerArgs { @@ -110,8 +125,11 @@ export interface DataLayerArgs { columnToLabel?: string; // Actually a JSON key-value pair xScaleType: XScaleType; isHistogram: boolean; + isPercentage: boolean; + isStacked: boolean; + isHorizontal: boolean; palette: PaletteOutput; - yConfig?: YConfigResult[]; + decorations?: DataDecorationConfigResult[]; } export interface ValidLayer extends DataLayerConfigResult { @@ -133,9 +151,12 @@ export interface ExtendedDataLayerArgs { columnToLabel?: string; // Actually a JSON key-value pair xScaleType: XScaleType; isHistogram: boolean; + isPercentage: boolean; + isStacked: boolean; + isHorizontal: boolean; palette: PaletteOutput; // palette will always be set on the expression - yConfig?: YConfigResult[]; + decorations?: DataDecorationConfigResult[]; table?: Datatable; } @@ -185,22 +206,8 @@ export interface LegendConfig { legendSize?: LegendSize; } -export interface LabelsOrientationConfig { - x: number; - yLeft: number; - yRight: number; -} - // Arguments to XY chart expression, with computed properties export interface XYArgs extends DataLayerArgs { - xTitle: string; - yTitle: string; - yRightTitle: string; - xExtent?: AxisExtentConfigResult; - yLeftExtent: AxisExtentConfigResult; - yRightExtent: AxisExtentConfigResult; - yLeftScale: YScaleType; - yRightScale: YScaleType; legend: LegendConfigResult; endValue?: EndValue; emphasizeFitting?: boolean; @@ -208,15 +215,13 @@ export interface XYArgs extends DataLayerArgs { referenceLines: ReferenceLineConfigResult[]; annotationLayers: AnnotationLayerConfigResult[]; fittingFunction?: FittingFunction; - axisTitlesVisibilitySettings?: AxisTitlesVisibilityConfigResult; - tickLabelsVisibilitySettings?: TickLabelsConfigResult; - gridlinesVisibilitySettings?: GridlinesConfigResult; - labelsOrientation?: LabelsOrientationConfigResult; curveType?: XYCurveType; fillOpacity?: number; hideEndzones?: boolean; valuesInLegend?: boolean; ariaLabel?: string; + yAxisConfigs?: YAxisConfigResult[]; + xAxisConfig?: XAxisConfigResult; addTimeMarker?: boolean; markSizeRatio?: number; minTimeBarInterval?: string; @@ -228,29 +233,19 @@ export interface XYArgs extends DataLayerArgs { } export interface LayeredXYArgs { - xTitle: string; - yTitle: string; - yRightTitle: string; - xExtent?: AxisExtentConfigResult; - yLeftExtent: AxisExtentConfigResult; - yRightExtent: AxisExtentConfigResult; - yLeftScale: YScaleType; - yRightScale: YScaleType; legend: LegendConfigResult; endValue?: EndValue; emphasizeFitting?: boolean; valueLabels: ValueLabelMode; layers?: XYExtendedLayerConfigResult[]; fittingFunction?: FittingFunction; - axisTitlesVisibilitySettings?: AxisTitlesVisibilityConfigResult; - tickLabelsVisibilitySettings?: TickLabelsConfigResult; - gridlinesVisibilitySettings?: GridlinesConfigResult; - labelsOrientation?: LabelsOrientationConfigResult; curveType?: XYCurveType; fillOpacity?: number; hideEndzones?: boolean; valuesInLegend?: boolean; ariaLabel?: string; + yAxisConfigs?: YAxisConfigResult[]; + xAxisConfig?: XAxisConfigResult; detailedTooltip?: boolean; addTimeMarker?: boolean; markSizeRatio?: number; @@ -260,29 +255,19 @@ export interface LayeredXYArgs { } export interface XYProps { - xTitle: string; - yTitle: string; - yRightTitle: string; - xExtent?: AxisExtentConfigResult; - yLeftExtent: AxisExtentConfigResult; - yRightExtent: AxisExtentConfigResult; - yLeftScale: YScaleType; - yRightScale: YScaleType; legend: LegendConfigResult; endValue?: EndValue; emphasizeFitting?: boolean; valueLabels: ValueLabelMode; layers: CommonXYLayerConfig[]; fittingFunction?: FittingFunction; - axisTitlesVisibilitySettings?: AxisTitlesVisibilityConfigResult; - tickLabelsVisibilitySettings?: TickLabelsConfigResult; - gridlinesVisibilitySettings?: GridlinesConfigResult; - labelsOrientation?: LabelsOrientationConfigResult; curveType?: XYCurveType; fillOpacity?: number; hideEndzones?: boolean; valuesInLegend?: boolean; ariaLabel?: string; + yAxisConfigs?: YAxisConfigResult[]; + xAxisConfig?: XAxisConfigResult; addTimeMarker?: boolean; markSizeRatio?: number; minTimeBarInterval?: string; @@ -312,7 +297,8 @@ export type ExtendedAnnotationLayerConfigResult = ExtendedAnnotationLayerArgs & layerType: typeof LayerTypes.ANNOTATIONS; }; -export interface ReferenceLineArgs extends Omit { +export interface ReferenceLineArgs + extends Omit { name?: string; value: number; fill: FillStyle; @@ -322,7 +308,7 @@ export interface ReferenceLineLayerArgs { layerId?: string; accessors: string[]; columnToLabel?: string; - yConfig?: ExtendedYConfigResult[]; + decorations?: ReferenceLineDecorationConfigResult[]; table?: Datatable; } @@ -338,15 +324,15 @@ export type XYExtendedLayerConfigResult = | ReferenceLineLayerConfigResult | ExtendedAnnotationLayerConfigResult; -export interface ReferenceLineYConfig extends ReferenceLineArgs { - type: typeof REFERENCE_LINE_Y_CONFIG; +export interface ExtendedReferenceLineDecorationConfig extends ReferenceLineArgs { + type: typeof EXTENDED_REFERENCE_LINE_DECORATION_CONFIG; } export interface ReferenceLineConfigResult { type: typeof REFERENCE_LINE; layerType: typeof LayerTypes.REFERENCELINE; lineLength: number; - yConfig: [ReferenceLineYConfig]; + decorations: [ExtendedReferenceLineDecorationConfig]; } export type ReferenceLineLayerConfigResult = ReferenceLineLayerArgs & { @@ -381,21 +367,18 @@ export type ExtendedDataLayerConfigResult = Omit >; -export type YConfigFn = ExpressionFunctionDefinition; -export type ExtendedYConfigFn = ExpressionFunctionDefinition< - typeof EXTENDED_Y_CONFIG, +export type DataDecorationConfigFn = ExpressionFunctionDefinition< + typeof DATA_DECORATION_CONFIG, null, - ExtendedYConfig, - ExtendedYConfigResult + DataDecorationConfig, + DataDecorationConfigResult +>; +export type ReferenceLineDecorationConfigFn = ExpressionFunctionDefinition< + typeof REFERENCE_LINE_DECORATION_CONFIG, + null, + ReferenceLineDecorationConfig, + ReferenceLineDecorationConfigResult >; export type LegendConfigFn = ExpressionFunctionDefinition< @@ -455,3 +443,17 @@ export type LegendConfigFn = ExpressionFunctionDefinition< LegendConfig, Promise >; + +export type XAxisConfigFn = ExpressionFunctionDefinition< + typeof X_AXIS_CONFIG, + null, + AxisConfig, + XAxisConfigResult +>; + +export type YAxisConfigFn = ExpressionFunctionDefinition< + typeof Y_AXIS_CONFIG, + null, + YAxisConfig, + YAxisConfigResult +>; diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts index b03ea975b0143..5a2ccb5df6d68 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts @@ -26,7 +26,7 @@ export interface XYRender { export interface CollectiveConfig extends Omit { roundedTimestamp: number; - axisMode: 'bottom'; + position: 'bottom'; icon?: AvailableAnnotationIcon | string; customTooltipDetails?: AnnotationTooltipFormatter | undefined; } diff --git a/src/plugins/chart_expressions/expression_xy/public/__mocks__/index.tsx b/src/plugins/chart_expressions/expression_xy/public/__mocks__/index.tsx index 2aab5f69a5cf3..a7c6494a33542 100644 --- a/src/plugins/chart_expressions/expression_xy/public/__mocks__/index.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/__mocks__/index.tsx @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { Position } from '@elastic/charts'; import { Datatable } from '@kbn/expressions-plugin/common'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { LayerTypes } from '../../common/constants'; @@ -167,8 +168,11 @@ export const dateHistogramLayer: DataLayerConfig = { xAccessor: 'xAccessorId', xScaleType: 'time', isHistogram: true, + isStacked: true, + isPercentage: false, + isHorizontal: false, splitAccessor: 'splitAccessorId', - seriesType: 'bar_stacked', + seriesType: 'bar', accessors: ['yAccessorId'], palette: mockPaletteOutput, table: dateHistogramData, @@ -197,7 +201,13 @@ export function sampleArgsWithReferenceLine(value: number = 150) { type: 'referenceLineLayer', layerType: LayerTypes.REFERENCELINE, accessors: ['referenceLine-a'], - yConfig: [{ axisMode: 'left', forAccessor: 'referenceLine-a', type: 'extendedYConfig' }], + decorations: [ + { + forAccessor: 'referenceLine-a', + type: 'referenceLineDecorationConfig', + position: Position.Left, + }, + ], table: data, }, ], diff --git a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap index be930b69634df..eb0379d15be04 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap @@ -18,8 +18,8 @@ exports[`XYChart component annotations should render basic line annotation 1`] = 1) { const commonStyles = getCommonStyles(configArr); diff --git a/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx b/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx index b19cf515d43b8..919e336ebbc94 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/data_layers.tsx @@ -25,7 +25,7 @@ import { XYCurveType, XScaleType, } from '../../common'; -import { SeriesTypes, ValueLabelModes } from '../../common/constants'; +import { SeriesTypes, ValueLabelModes, AxisModes } from '../../common/constants'; import { getColorAssignments, getFitOptions, @@ -90,12 +90,14 @@ export const DataLayers: FC = ({ // In order to do it we need to make a copy of the table as the raw one is required for more features (filters, etc...) later on const formattedDatatableInfo = formattedDatatables[layerId]; - const isPercentage = seriesType.includes('percentage'); - const yAxis = yAxesConfiguration.find((axisConfiguration) => axisConfiguration.series.find((currentSeries) => currentSeries.accessor === yColumnId) ); + const isPercentage = yAxis?.mode + ? yAxis?.mode === AxisModes.PERCENTAGE + : layer.isPercentage; + const seriesProps = getSeriesProps({ layer, titles: titles[layer.layerId], @@ -129,11 +131,6 @@ export const DataLayers: FC = ({ /> ); case SeriesTypes.BAR: - case SeriesTypes.BAR_STACKED: - case SeriesTypes.BAR_PERCENTAGE_STACKED: - case SeriesTypes.BAR_HORIZONTAL: - case SeriesTypes.BAR_HORIZONTAL_STACKED: - case SeriesTypes.BAR_HORIZONTAL_PERCENTAGE_STACKED: const valueLabelsSettings = { displayValueSettings: { // This format double fixes two issues in elastic-chart @@ -150,22 +147,12 @@ export const DataLayers: FC = ({ }, }; return ; - case SeriesTypes.AREA_STACKED: - case SeriesTypes.AREA_PERCENTAGE_STACKED: - return ( - - ); case SeriesTypes.AREA: return ( ); diff --git a/src/plugins/chart_expressions/expression_xy/public/components/legend_action.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/legend_action.test.tsx index 35b5b2ac0eb09..147338853a808 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/legend_action.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/legend_action.test.tsx @@ -157,6 +157,9 @@ const sampleLayer: DataLayerConfig = { type: 'dataLayer', layerType: LayerTypes.DATA, seriesType: 'line', + isStacked: false, + isPercentage: false, + isHorizontal: false, showLines: true, xAccessor: 'c', accessors: ['a', 'b'], diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line.tsx index 30f4a97986ec3..6c58171d35ae4 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line.tsx @@ -10,47 +10,49 @@ import React, { FC } from 'react'; import { Position } from '@elastic/charts'; import { FieldFormat } from '@kbn/field-formats-plugin/common'; import { ReferenceLineConfig } from '../../../common/types'; -import { getGroupId } from './utils'; import { ReferenceLineAnnotations } from './reference_line_annotations'; +import { AxesMap, GroupsConfiguration } from '../../helpers'; +import { getAxisGroupForReferenceLine } from './utils'; interface ReferenceLineProps { layer: ReferenceLineConfig; paddingMap: Partial>; - formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; - axesMap: Record<'left' | 'right', boolean>; + xAxisFormatter: FieldFormat; + axesConfiguration: GroupsConfiguration; isHorizontal: boolean; nextValue?: number; + yAxesMap: AxesMap; } export const ReferenceLine: FC = ({ layer, - axesMap, - formatters, + axesConfiguration, + xAxisFormatter, paddingMap, isHorizontal, nextValue, + yAxesMap, }) => { const { - yConfig: [yConfig], + decorations: [decorationConfig], } = layer; - if (!yConfig) { + if (!decorationConfig) { return null; } - const { axisMode, value } = yConfig; + const { value } = decorationConfig; - // Find the formatter for the given axis - const groupId = getGroupId(axisMode); + const axisGroup = getAxisGroupForReferenceLine(axesConfiguration, decorationConfig, isHorizontal); - const formatter = formatters[groupId || 'bottom']; + const formatter = axisGroup?.formatter || xAxisFormatter; const id = `${layer.layerId}-${value}`; return ( diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line_annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line_annotations.tsx index b5b94b4c2df51..b97c105e2ab3e 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line_annotations.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_line_annotations.tsx @@ -10,18 +10,21 @@ import { AnnotationDomainType, LineAnnotation, Position, RectAnnotation } from ' import { euiLightVars } from '@kbn/ui-theme'; import React, { FC } from 'react'; import { FieldFormat } from '@kbn/field-formats-plugin/common'; -import { LINES_MARKER_SIZE } from '../../helpers'; +import { + AxesMap, + AxisConfiguration, + getOriginalAxisPosition, + LINES_MARKER_SIZE, +} from '../../helpers'; import { AvailableReferenceLineIcon, FillStyle, IconPosition, LineStyle, - YAxisMode, } from '../../../common/types'; import { getBaseIconPlacement, getBottomRect, - getGroupId, getHorizontalRect, getLineAnnotationProps, getSharedStyle, @@ -38,7 +41,7 @@ export interface ReferenceLineAnnotationConfig { fill?: FillStyle; iconPosition?: IconPosition; textVisibility?: boolean; - axisMode?: YAxisMode; + axisGroup?: AxisConfiguration; color?: string; } @@ -46,18 +49,19 @@ interface Props { config: ReferenceLineAnnotationConfig; paddingMap: Partial>; formatter?: FieldFormat; - axesMap: Record<'left' | 'right', boolean>; + axesMap: AxesMap; isHorizontal: boolean; } const getRectDataValue = ( annotationConfig: ReferenceLineAnnotationConfig, - formatter: FieldFormat | undefined + formatter: FieldFormat | undefined, + groupId: string ) => { - const { name, value, nextValue, fill, axisMode } = annotationConfig; + const { name, value, nextValue, fill } = annotationConfig; const isFillAbove = fill === 'above'; - if (axisMode === 'bottom') { + if (groupId === Position.Bottom) { return getBottomRect(name, isFillAbove, formatter, value, nextValue); } @@ -71,13 +75,15 @@ export const ReferenceLineAnnotations: FC = ({ paddingMap, isHorizontal, }) => { - const { id, axisMode, iconPosition, name, textVisibility, value, fill, color } = config; + const { id, axisGroup, iconPosition, name, textVisibility, value, fill, color } = config; - // Find the formatter for the given axis - const groupId = getGroupId(axisMode); const defaultColor = euiLightVars.euiColorDarkShade; // get the position for vertical chart - const markerPositionVertical = getBaseIconPlacement(iconPosition, axesMap, axisMode); + const markerPositionVertical = getBaseIconPlacement( + iconPosition, + axesMap, + getOriginalAxisPosition(axisGroup?.position ?? Position.Bottom, isHorizontal) + ); // the padding map is built for vertical chart const hasReducedPadding = paddingMap[markerPositionVertical] === LINES_MARKER_SIZE; @@ -89,7 +95,6 @@ export const ReferenceLineAnnotations: FC = ({ }, axesMap, paddingMap, - groupId, isHorizontal ); @@ -108,7 +113,9 @@ export const ReferenceLineAnnotations: FC = ({ key={`${id}-line`} dataValues={[dataValues]} domainType={ - axisMode === 'bottom' ? AnnotationDomainType.XDomain : AnnotationDomainType.YDomain + props.groupId === Position.Bottom + ? AnnotationDomainType.XDomain + : AnnotationDomainType.YDomain } style={{ line: { ...sharedStyle, opacity: 1 } }} /> @@ -116,7 +123,7 @@ export const ReferenceLineAnnotations: FC = ({ let rect; if (fill && fill !== 'none') { - const rectDataValues = getRectDataValue(config, formatter); + const rectDataValues = getRectDataValue(config, formatter, props.groupId); rect = ( ; paddingMap: Partial>; - axesMap: Record<'left' | 'right', boolean>; isHorizontal: boolean; titles?: LayerAccessorsTitles; + xAxisFormatter: FieldFormat; + axesConfiguration: GroupsConfiguration; + yAxesMap: AxesMap; } export const ReferenceLineLayer: FC = ({ layer, - formatters, + axesConfiguration, + xAxisFormatter, paddingMap, - axesMap, isHorizontal, titles, + yAxesMap, }) => { - if (!layer.yConfig) { + if (!layer.decorations) { return null; } - const { columnToLabel, yConfig: yConfigs, table } = layer; + const { columnToLabel, decorations, table } = layer; const columnToLabelMap: Record = columnToLabel ? JSON.parse(columnToLabel) : {}; const row = table.rows[0]; - const yConfigByValue = yConfigs.sort( + const decorationConfigsByValue = decorations.sort( ({ forAccessor: idA }, { forAccessor: idB }) => row[idA] - row[idB] ); - const groupedByDirection = groupBy(yConfigByValue, 'fill'); + const groupedByDirection = groupBy(decorationConfigsByValue, 'fill'); if (groupedByDirection.below) { groupedByDirection.below.reverse(); } - const referenceLineElements = yConfigByValue.flatMap((yConfig) => { - const { axisMode } = yConfig; - - // Find the formatter for the given axis - const groupId = getGroupId(axisMode); + const referenceLineElements = decorationConfigsByValue.flatMap((decorationConfig) => { + const axisGroup = getAxisGroupForReferenceLine( + axesConfiguration, + decorationConfig, + isHorizontal + ); - const formatter = formatters[groupId || 'bottom']; - const name = columnToLabelMap[yConfig.forAccessor] ?? titles?.yTitles?.[yConfig.forAccessor]; - const value = row[yConfig.forAccessor]; - const yConfigsWithSameDirection = groupedByDirection[yConfig.fill!]; - const indexFromSameType = yConfigsWithSameDirection.findIndex( - ({ forAccessor }) => forAccessor === yConfig.forAccessor + const formatter = axisGroup?.formatter || xAxisFormatter; + const name = + columnToLabelMap[decorationConfig.forAccessor] ?? + titles?.yTitles?.[decorationConfig.forAccessor]; + const value = row[decorationConfig.forAccessor]; + const yDecorationsWithSameDirection = groupedByDirection[decorationConfig.fill!]; + const indexFromSameType = yDecorationsWithSameDirection.findIndex( + ({ forAccessor }) => forAccessor === decorationConfig.forAccessor ); - const shouldCheckNextReferenceLine = indexFromSameType < yConfigsWithSameDirection.length - 1; + const shouldCheckNextReferenceLine = + indexFromSameType < yDecorationsWithSameDirection.length - 1; const nextValue = shouldCheckNextReferenceLine - ? row[yConfigsWithSameDirection[indexFromSameType + 1].forAccessor] + ? row[yDecorationsWithSameDirection[indexFromSameType + 1].forAccessor] : undefined; - const { forAccessor, type, ...restAnnotationConfig } = yConfig; - const id = `${layer.layerId}-${yConfig.forAccessor}`; + const { forAccessor, type, ...restAnnotationConfig } = decorationConfig; + const id = `${layer.layerId}-${decorationConfig.forAccessor}`; return ( = ({ nextValue, name, ...restAnnotationConfig, + axisGroup, }} + axesMap={yAxesMap} paddingMap={paddingMap} - axesMap={axesMap} formatter={formatter} isHorizontal={isHorizontal} /> diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.test.tsx index ec657ee293e69..d82bd236b2320 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.test.tsx @@ -15,7 +15,7 @@ import { LayerTypes } from '../../../common/constants'; import { ReferenceLineLayerArgs, ReferenceLineLayerConfig, - ExtendedYConfig, + ExtendedReferenceLineDecorationConfig, ReferenceLineArgs, ReferenceLineConfig, } from '../../../common/types'; @@ -46,12 +46,14 @@ const data: Datatable = { })), }; -function createLayers(yConfigs: ReferenceLineLayerArgs['yConfig']): ReferenceLineLayerConfig[] { +function createLayers( + decorations: ReferenceLineLayerArgs['decorations'] +): ReferenceLineLayerConfig[] { return [ { layerId: 'first', - accessors: (yConfigs || []).map(({ forAccessor }) => forAccessor), - yConfig: yConfigs, + accessors: (decorations || []).map(({ forAccessor }) => forAccessor), + decorations, type: 'referenceLineLayer', layerType: LayerTypes.REFERENCELINE, table: data, @@ -69,7 +71,7 @@ function createReferenceLine( type: 'referenceLine', layerType: 'referenceLine', lineLength, - yConfig: [{ type: 'referenceLineYConfig', ...args }], + decorations: [{ type: 'extendedReferenceLineDecorationConfig', ...args }], }; } @@ -82,7 +84,7 @@ interface XCoords { x1: number | undefined; } -function getAxisFromId(layerPrefix: string): ExtendedYConfig['axisMode'] { +function getAxisFromId(layerPrefix: string): ExtendedReferenceLineDecorationConfig['position'] { return /left/i.test(layerPrefix) ? 'left' : /right/i.test(layerPrefix) ? 'right' : 'bottom'; } @@ -90,21 +92,42 @@ const emptyCoords = { x0: undefined, x1: undefined, y0: undefined, y1: undefined describe('ReferenceLines', () => { describe('referenceLineLayers', () => { - let formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; let defaultProps: Omit; beforeEach(() => { - formatters = { - left: { convert: jest.fn((x) => x) } as unknown as FieldFormat, - right: { convert: jest.fn((x) => x) } as unknown as FieldFormat, - bottom: { convert: jest.fn((x) => x) } as unknown as FieldFormat, - }; - defaultProps = { - formatters, + xAxisFormatter: { convert: jest.fn((x) => x) } as unknown as FieldFormat, isHorizontal: false, - axesMap: { left: true, right: false }, + axesConfiguration: [ + { + groupId: 'left', + position: 'left', + series: [], + }, + { + groupId: 'right', + position: 'right', + series: [], + }, + { + groupId: 'bottom', + position: 'bottom', + series: [], + }, + ], paddingMap: {}, + yAxesMap: { + left: { + groupId: 'left', + position: 'left', + series: [], + }, + right: { + groupId: 'right', + position: 'right', + series: [], + }, + }, }; }); @@ -113,20 +136,20 @@ describe('ReferenceLines', () => { ['yAccessorLeft', 'below'], ['yAccessorRight', 'above'], ['yAccessorRight', 'below'], - ] as Array<[string, Exclude]>)( + ] as Array<[string, Exclude]>)( 'should render a RectAnnotation for a reference line with fill set: %s %s', (layerPrefix, fill) => { - const axisMode = getAxisFromId(layerPrefix); + const position = getAxisFromId(layerPrefix); const wrapper = shallow( @@ -154,7 +177,7 @@ describe('ReferenceLines', () => { it.each([ ['xAccessor', 'above'], ['xAccessor', 'below'], - ] as Array<[string, Exclude]>)( + ] as Array<[string, Exclude]>)( 'should render a RectAnnotation for a reference line with fill set: %s %s', (layerPrefix, fill) => { const wrapper = shallow( @@ -163,9 +186,9 @@ describe('ReferenceLines', () => { layers={createLayers([ { forAccessor: `${layerPrefix}FirstId`, - axisMode: 'bottom', + position: 'bottom', lineStyle: 'solid', - type: 'extendedYConfig', + type: 'referenceLineDecorationConfig', fill, }, ])} @@ -196,26 +219,26 @@ describe('ReferenceLines', () => { ['yAccessorLeft', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], ['yAccessorRight', 'above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], ['yAccessorRight', 'below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], - ] as Array<[string, Exclude, YCoords, YCoords]>)( + ] as Array<[string, Exclude, YCoords, YCoords]>)( 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', (layerPrefix, fill, coordsA, coordsB) => { - const axisMode = getAxisFromId(layerPrefix); + const position = getAxisFromId(layerPrefix); const wrapper = shallow( { it.each([ ['xAccessor', 'above', { x0: 1, x1: 2 }, { x0: 2, x1: undefined }], ['xAccessor', 'below', { x0: undefined, x1: 1 }, { x0: 1, x1: 2 }], - ] as Array<[string, Exclude, XCoords, XCoords]>)( + ] as Array<[string, Exclude, XCoords, XCoords]>)( 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', (layerPrefix, fill, coordsA, coordsB) => { const wrapper = shallow( @@ -261,16 +284,16 @@ describe('ReferenceLines', () => { layers={createLayers([ { forAccessor: `${layerPrefix}FirstId`, - axisMode: 'bottom', + position: 'bottom', lineStyle: 'solid', - type: 'extendedYConfig', + type: 'referenceLineDecorationConfig', fill, }, { forAccessor: `${layerPrefix}SecondId`, - axisMode: 'bottom', + position: 'bottom', lineStyle: 'solid', - type: 'extendedYConfig', + type: 'referenceLineDecorationConfig', fill, }, ])} @@ -307,7 +330,7 @@ describe('ReferenceLines', () => { it.each(['yAccessorLeft', 'yAccessorRight', 'xAccessor'])( 'should let areas in different directions overlap: %s', (layerPrefix) => { - const axisMode = getAxisFromId(layerPrefix); + const position = getAxisFromId(layerPrefix); const wrapper = shallow( { layers={createLayers([ { forAccessor: `${layerPrefix}FirstId`, - axisMode, + position, lineStyle: 'solid', fill: 'above', - type: 'extendedYConfig', + type: 'referenceLineDecorationConfig', }, { forAccessor: `${layerPrefix}SecondId`, - axisMode, + position, lineStyle: 'solid', fill: 'below', - type: 'extendedYConfig', + type: 'referenceLineDecorationConfig', }, ])} /> @@ -338,8 +361,8 @@ describe('ReferenceLines', () => { ).toEqual( expect.arrayContaining([ { - coordinates: { ...emptyCoords, ...(axisMode === 'bottom' ? { x0: 1 } : { y0: 5 }) }, - details: axisMode === 'bottom' ? 1 : 5, + coordinates: { ...emptyCoords, ...(position === 'bottom' ? { x0: 1 } : { y0: 5 }) }, + details: position === 'bottom' ? 1 : 5, header: undefined, }, ]) @@ -349,8 +372,8 @@ describe('ReferenceLines', () => { ).toEqual( expect.arrayContaining([ { - coordinates: { ...emptyCoords, ...(axisMode === 'bottom' ? { x1: 2 } : { y1: 10 }) }, - details: axisMode === 'bottom' ? 2 : 10, + coordinates: { ...emptyCoords, ...(position === 'bottom' ? { x1: 2 } : { y1: 10 }) }, + details: position === 'bottom' ? 2 : 10, header: undefined, }, ]) @@ -361,7 +384,7 @@ describe('ReferenceLines', () => { it.each([ ['above', { y0: 5, y1: 10 }, { y0: 10, y1: undefined }], ['below', { y0: undefined, y1: 5 }, { y0: 5, y1: 10 }], - ] as Array<[Exclude, YCoords, YCoords]>)( + ] as Array<[Exclude, YCoords, YCoords]>)( 'should be robust and works also for different axes when on same direction: 1x Left + 1x Right both %s', (fill, coordsA, coordsB) => { const wrapper = shallow( @@ -370,17 +393,17 @@ describe('ReferenceLines', () => { layers={createLayers([ { forAccessor: `yAccessorLeftFirstId`, - axisMode: 'left', + position: 'left', lineStyle: 'solid', fill, - type: 'extendedYConfig', + type: 'referenceLineDecorationConfig', }, { forAccessor: `yAccessorRightSecondId`, - axisMode: 'right', + position: 'right', lineStyle: 'solid', fill, - type: 'extendedYConfig', + type: 'referenceLineDecorationConfig', }, ])} /> @@ -415,21 +438,42 @@ describe('ReferenceLines', () => { }); describe('referenceLines', () => { - let formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; let defaultProps: Omit; beforeEach(() => { - formatters = { - left: { convert: jest.fn((x) => x) } as unknown as FieldFormat, - right: { convert: jest.fn((x) => x) } as unknown as FieldFormat, - bottom: { convert: jest.fn((x) => x) } as unknown as FieldFormat, - }; - defaultProps = { - formatters, + xAxisFormatter: { convert: jest.fn((x) => x) } as unknown as FieldFormat, isHorizontal: false, - axesMap: { left: true, right: false }, + axesConfiguration: [ + { + groupId: 'left', + position: 'left', + series: [], + }, + { + groupId: 'right', + position: 'right', + series: [], + }, + { + groupId: 'bottom', + position: 'bottom', + series: [], + }, + ], paddingMap: {}, + yAxesMap: { + left: { + groupId: 'left', + position: 'left', + series: [], + }, + right: { + groupId: 'right', + position: 'right', + series: [], + }, + }, }; }); @@ -438,17 +482,17 @@ describe('ReferenceLines', () => { ['yAccessorLeft', 'below'], ['yAccessorRight', 'above'], ['yAccessorRight', 'below'], - ] as Array<[string, Exclude]>)( + ] as Array<[string, Exclude]>)( 'should render a RectAnnotation for a reference line with fill set: %s %s', (layerPrefix, fill) => { - const axisMode = getAxisFromId(layerPrefix); + const position = getAxisFromId(layerPrefix); const value = 5; const wrapper = shallow( { it.each([ ['xAccessor', 'above'], ['xAccessor', 'below'], - ] as Array<[string, Exclude]>)( + ] as Array<[string, Exclude]>)( 'should render a RectAnnotation for a reference line with fill set: %s %s', (layerPrefix, fill) => { const value = 1; @@ -488,7 +532,7 @@ describe('ReferenceLines', () => { {...defaultProps} layers={[ createReferenceLine(layerPrefix, 1, { - axisMode: 'bottom', + position: 'bottom', lineStyle: 'solid', fill, value, @@ -519,23 +563,23 @@ describe('ReferenceLines', () => { it.each([ ['yAccessorLeft', 'above', { y0: 10, y1: undefined }, { y0: 10, y1: undefined }], ['yAccessorLeft', 'below', { y0: undefined, y1: 5 }, { y0: undefined, y1: 5 }], - ] as Array<[string, Exclude, YCoords, YCoords]>)( + ] as Array<[string, Exclude, YCoords, YCoords]>)( 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', (layerPrefix, fill, coordsA, coordsB) => { - const axisMode = getAxisFromId(layerPrefix); + const position = getAxisFromId(layerPrefix); const value = coordsA.y0 ?? coordsA.y1!; const wrapper = shallow( { it.each([ ['xAccessor', 'above', { x0: 1, x1: undefined }, { x0: 1, x1: undefined }], ['xAccessor', 'below', { x0: undefined, x1: 1 }, { x0: undefined, x1: 1 }], - ] as Array<[string, Exclude, XCoords, XCoords]>)( + ] as Array<[string, Exclude, XCoords, XCoords]>)( 'should avoid overlap between two reference lines with fill in the same direction: 2 x %s %s', (layerPrefix, fill, coordsA, coordsB) => { const value = coordsA.x0 ?? coordsA.x1!; @@ -579,13 +623,13 @@ describe('ReferenceLines', () => { {...defaultProps} layers={[ createReferenceLine(layerPrefix, 10, { - axisMode: 'bottom', + position: 'bottom', lineStyle: 'solid', fill, value, }), createReferenceLine(layerPrefix, 10, { - axisMode: 'bottom', + position: 'bottom', lineStyle: 'solid', fill, value, @@ -624,7 +668,7 @@ describe('ReferenceLines', () => { it.each(['yAccessorLeft', 'yAccessorRight', 'xAccessor'])( 'should let areas in different directions overlap: %s', (layerPrefix) => { - const axisMode = getAxisFromId(layerPrefix); + const position = getAxisFromId(layerPrefix); const value1 = 1; const value2 = 10; const wrapper = shallow( @@ -632,13 +676,13 @@ describe('ReferenceLines', () => { {...defaultProps} layers={[ createReferenceLine(layerPrefix, 10, { - axisMode, + position, lineStyle: 'solid', fill: 'above', value: value1, }), createReferenceLine(layerPrefix, 10, { - axisMode, + position, lineStyle: 'solid', fill: 'below', value: value2, @@ -654,7 +698,7 @@ describe('ReferenceLines', () => { { coordinates: { ...emptyCoords, - ...(axisMode === 'bottom' ? { x0: value1 } : { y0: value1 }), + ...(position === 'bottom' ? { x0: value1 } : { y0: value1 }), }, details: value1, header: undefined, @@ -670,7 +714,7 @@ describe('ReferenceLines', () => { { coordinates: { ...emptyCoords, - ...(axisMode === 'bottom' ? { x1: value2 } : { y1: value2 }), + ...(position === 'bottom' ? { x1: value2 } : { y1: value2 }), }, details: value2, header: undefined, diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.tsx index a95d1942e4659..bb250c0a133bf 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/reference_lines.tsx @@ -12,18 +12,24 @@ import React from 'react'; import { Position } from '@elastic/charts'; import type { FieldFormat } from '@kbn/field-formats-plugin/common'; import type { CommonXYReferenceLineLayerConfig, ReferenceLineConfig } from '../../../common/types'; -import { isReferenceLine, LayersAccessorsTitles } from '../../helpers'; +import { + AxesMap, + GroupsConfiguration, + isReferenceLine, + LayersAccessorsTitles, +} from '../../helpers'; import { ReferenceLineLayer } from './reference_line_layer'; import { ReferenceLine } from './reference_line'; import { getNextValuesForReferenceLines } from './utils'; export interface ReferenceLinesProps { layers: CommonXYReferenceLineLayerConfig[]; - formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; - axesMap: Record<'left' | 'right', boolean>; + xAxisFormatter: FieldFormat; + axesConfiguration: GroupsConfiguration; isHorizontal: boolean; paddingMap: Partial>; titles?: LayersAccessorsTitles; + yAxesMap: AxesMap; } export const ReferenceLines = ({ layers, titles = {}, ...rest }: ReferenceLinesProps) => { @@ -36,13 +42,13 @@ export const ReferenceLines = ({ layers, titles = {}, ...rest }: ReferenceLinesP return ( <> {layers.flatMap((layer) => { - if (!layer.yConfig) { + if (!layer.decorations) { return null; } const key = `referenceLine-${layer.layerId}`; if (isReferenceLine(layer)) { - const nextValue = referenceLinesNextValues[layer.yConfig[0].fill][layer.layerId]; + const nextValue = referenceLinesNextValues[layer.decorations[0].fill][layer.layerId]; return ; } diff --git a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/utils.tsx b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/utils.tsx index 85d96c573f314..fab4f9a665526 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/utils.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/reference_lines/utils.tsx @@ -11,13 +11,23 @@ import { Position } from '@elastic/charts'; import { euiLightVars } from '@kbn/ui-theme'; import { FieldFormat } from '@kbn/field-formats-plugin/common'; import { groupBy, orderBy } from 'lodash'; -import { IconPosition, ReferenceLineConfig, YAxisMode, FillStyle } from '../../../common/types'; +import { + IconPosition, + ReferenceLineConfig, + FillStyle, + ExtendedReferenceLineDecorationConfig, + ReferenceLineDecorationConfig, +} from '../../../common/types'; import { FillStyles } from '../../../common/constants'; import { + GroupsConfiguration, LINES_MARKER_SIZE, mapVerticalToHorizontalPlacement, Marker, MarkerBody, + getAxisPosition, + getOriginalAxisPosition, + AxesMap, } from '../../helpers'; import { ReferenceLineAnnotationConfig } from './reference_line_annotations'; @@ -26,17 +36,18 @@ import { ReferenceLineAnnotationConfig } from './reference_line_annotations'; // this function assume the chart is vertical export function getBaseIconPlacement( iconPosition: IconPosition | undefined, - axesMap?: Record, - axisMode?: YAxisMode + axesMap?: AxesMap, + position?: Position ) { if (iconPosition === 'auto') { - if (axisMode === 'bottom') { + if (position === Position.Bottom) { return Position.Top; } if (axesMap) { - if (axisMode === 'left') { + if (position === Position.Left) { return axesMap.right ? Position.Left : Position.Right; } + return axesMap.left ? Position.Right : Position.Left; } } @@ -67,22 +78,26 @@ export const getSharedStyle = (config: ReferenceLineAnnotationConfig) => ({ export const getLineAnnotationProps = ( config: ReferenceLineAnnotationConfig, labels: { markerLabel?: string; markerBodyLabel?: string }, - axesMap: Record<'left' | 'right', boolean>, + axesMap: AxesMap, paddingMap: Partial>, - groupId: 'left' | 'right' | undefined, isHorizontal: boolean ) => { // get the position for vertical chart const markerPositionVertical = getBaseIconPlacement( config.iconPosition, axesMap, - config.axisMode + getOriginalAxisPosition(config.axisGroup?.position ?? Position.Bottom, isHorizontal) ); + // the padding map is built for vertical chart const hasReducedPadding = paddingMap[markerPositionVertical] === LINES_MARKER_SIZE; + const markerPosition = isHorizontal + ? mapVerticalToHorizontalPlacement(markerPositionVertical) + : markerPositionVertical; + return { - groupId, + groupId: config.axisGroup?.groupId || 'bottom', marker: ( ), // rotate the position if required - markerPosition: isHorizontal - ? mapVerticalToHorizontalPlacement(markerPositionVertical) - : markerPositionVertical, + markerPosition, }; }; -export const getGroupId = (axisMode: YAxisMode | undefined) => - axisMode === 'bottom' ? undefined : axisMode === 'right' ? 'right' : 'left'; - export const getBottomRect = ( headerLabel: string | undefined, isFillAbove: boolean, @@ -147,13 +154,16 @@ export const getHorizontalRect = ( const sortReferenceLinesByGroup = (referenceLines: ReferenceLineConfig[], group: FillStyle) => { if (group === FillStyles.ABOVE || group === FillStyles.BELOW) { const order = group === FillStyles.ABOVE ? 'asc' : 'desc'; - return orderBy(referenceLines, ({ yConfig: [{ value }] }) => value, [order]); + return orderBy(referenceLines, ({ decorations: [{ value }] }) => value, [order]); } return referenceLines; }; export const getNextValuesForReferenceLines = (referenceLines: ReferenceLineConfig[]) => { - const grouppedReferenceLines = groupBy(referenceLines, ({ yConfig: [yConfig] }) => yConfig.fill); + const grouppedReferenceLines = groupBy( + referenceLines, + ({ decorations: [decorationConfig] }) => decorationConfig.fill + ); const groups = Object.keys(grouppedReferenceLines) as FillStyle[]; return groups.reduce>>( @@ -164,8 +174,8 @@ export const getNextValuesForReferenceLines = (referenceLines: ReferenceLineConf (nextValues, referenceLine, index, lines) => { let nextValue: number | undefined; if (index < lines.length - 1) { - const [yConfig] = lines[index + 1].yConfig; - nextValue = yConfig.value; + const [decorationConfig] = lines[index + 1].decorations; + nextValue = decorationConfig.value; } return { ...nextValues, [referenceLine.layerId]: nextValue }; @@ -183,7 +193,7 @@ export const computeChartMargins = ( referenceLinePaddings: Partial>, labelVisibility: Partial>, titleVisibility: Partial>, - axesMap: Record<'left' | 'right', unknown>, + axesMap: AxesMap, isHorizontal: boolean ) => { const result: Partial> = {}; @@ -210,5 +220,18 @@ export const computeChartMargins = ( const placement = isHorizontal ? mapVerticalToHorizontalPlacement('top') : 'top'; result[placement] = referenceLinePaddings.top; } + return result; }; + +export function getAxisGroupForReferenceLine( + axesConfiguration: GroupsConfiguration, + decorationConfig: ReferenceLineDecorationConfig | ExtendedReferenceLineDecorationConfig, + shouldRotate: boolean +) { + return axesConfiguration.find( + (axis) => + (decorationConfig.axisId && axis.groupId.includes(decorationConfig.axisId)) || + getAxisPosition(decorationConfig.position ?? Position.Left, shouldRotate) === axis.position + ); +} diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx index 6353e22e7e5e4..024d43819c1b0 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx @@ -155,6 +155,9 @@ describe('XYChart component', () => { type: 'dataLayer', layerType: LayerTypes.DATA, seriesType: 'line', + isPercentage: false, + isHorizontal: false, + isStacked: false, xAccessor: 'c', accessors: ['a', 'b'], showLines: true, @@ -255,6 +258,9 @@ describe('XYChart component', () => { layerType: LayerTypes.DATA, showLines: true, seriesType: 'line', + isHorizontal: false, + isStacked: false, + isPercentage: false, xAccessor: 'c', accessors: ['a', 'b'], splitAccessor: 'd', @@ -339,7 +345,8 @@ describe('XYChart component', () => { test('it should enable the new time axis for a stacked vertical bar with break down dimension', () => { const timeLayer: DataLayerConfig = { ...defaultTimeLayer, - seriesType: 'bar_stacked', + seriesType: 'bar', + isStacked: true, }; const timeLayerArgs = createArgsWithLayers([timeLayer]); @@ -514,11 +521,14 @@ describe('XYChart component', () => { isHistogram: true, }, ], - xExtent: { - type: 'axisExtentConfig', - mode: 'custom', - lowerBound: 123, - upperBound: 456, + xAxisConfig: { + type: 'xAxisConfig', + extent: { + type: 'axisExtentConfig', + mode: 'custom', + lowerBound: 123, + upperBound: 456, + }, }, }} /> @@ -541,12 +551,18 @@ describe('XYChart component', () => { {...defaultProps} args={{ ...args, - yLeftExtent: { - type: 'axisExtentConfig', - mode: 'custom', - lowerBound: 123, - upperBound: 456, - }, + yAxisConfigs: [ + { + type: 'yAxisConfig', + position: 'left', + extent: { + type: 'axisExtentConfig', + mode: 'custom', + lowerBound: 123, + upperBound: 456, + }, + }, + ], }} /> ); @@ -564,10 +580,16 @@ describe('XYChart component', () => { {...defaultProps} args={{ ...args, - yLeftExtent: { - type: 'axisExtentConfig', - mode: 'dataBounds', - }, + yAxisConfigs: [ + { + type: 'yAxisConfig', + position: 'left', + extent: { + type: 'axisExtentConfig', + mode: 'dataBounds', + }, + }, + ], }} /> ); @@ -585,10 +607,16 @@ describe('XYChart component', () => { {...defaultProps} args={{ ...args, - yLeftExtent: { - type: 'axisExtentConfig', - mode: 'dataBounds', - }, + yAxisConfigs: [ + { + type: 'yAxisConfig', + position: 'left', + extent: { + type: 'axisExtentConfig', + mode: 'dataBounds', + }, + }, + ], layers: [ { ...(args.layers[0] as DataLayerConfig), @@ -612,16 +640,16 @@ describe('XYChart component', () => { {...defaultProps} args={{ ...args, - yLeftExtent: { - type: 'axisExtentConfig', - mode: 'custom', - lowerBound: 123, - upperBound: 456, - }, - layers: [ + yAxisConfigs: [ { - ...(args.layers[0] as DataLayerConfig), - seriesType: 'bar', + type: 'yAxisConfig', + position: 'left', + extent: { + type: 'axisExtentConfig', + mode: 'custom', + lowerBound: 123, + upperBound: 456, + }, }, ], }} @@ -629,8 +657,8 @@ describe('XYChart component', () => { ); expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ fit: false, - min: NaN, - max: NaN, + min: 123, + max: 456, includeDataFromIds: [], }); }); @@ -901,7 +929,9 @@ describe('XYChart component', () => { {...defaultProps} args={{ ...args, - layers: [{ ...(args.layers[0] as DataLayerConfig), seriesType: 'bar_horizontal' }], + layers: [ + { ...(args.layers[0] as DataLayerConfig), isHorizontal: true, seriesType: 'bar' }, + ], }} /> ); @@ -1020,7 +1050,10 @@ describe('XYChart component', () => { xAccessor: 'xAccessorId', xScaleType: 'linear', isHistogram: true, - seriesType: 'bar_stacked', + isHorizontal: false, + isStacked: true, + seriesType: 'bar', + isPercentage: false, accessors: ['yAccessorId'], palette: mockPaletteOutput, table: numberHistogramData, @@ -1091,8 +1124,11 @@ describe('XYChart component', () => { type: 'dataLayer', layerType: LayerTypes.DATA, isHistogram: true, + seriesType: 'bar', + isStacked: true, + isHorizontal: false, + isPercentage: false, showLines: true, - seriesType: 'bar_stacked', xAccessor: 'b', xScaleType: 'time', splitAccessor: 'b', @@ -1218,7 +1254,10 @@ describe('XYChart component', () => { xAccessor: 'xAccessorId', xScaleType: 'linear', isHistogram: true, - seriesType: 'bar_stacked', + isPercentage: false, + seriesType: 'bar', + isStacked: true, + isHorizontal: false, accessors: ['yAccessorId'], palette: mockPaletteOutput, table: numberHistogramData, @@ -1291,6 +1330,9 @@ describe('XYChart component', () => { type: 'dataLayer', layerType: LayerTypes.DATA, seriesType: 'line', + isHorizontal: false, + isStacked: false, + isPercentage: false, showLines: true, xAccessor: 'd', accessors: ['a', 'b'], @@ -1351,6 +1393,9 @@ describe('XYChart component', () => { layerType: LayerTypes.DATA, showLines: true, seriesType: 'line', + isHorizontal: false, + isStacked: false, + isPercentage: false, xAccessor: 'd', accessors: ['a', 'b'], columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', @@ -1382,6 +1427,9 @@ describe('XYChart component', () => { layerType: LayerTypes.DATA, showLines: true, seriesType: 'line', + isHorizontal: false, + isStacked: false, + isPercentage: false, xAccessor: 'd', accessors: ['a', 'b'], columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', @@ -1421,7 +1469,7 @@ describe('XYChart component', () => { {...defaultProps} args={{ ...args, - layers: [{ ...(args.layers[0] as DataLayerConfig), seriesType: 'bar_stacked' }], + layers: [{ ...(args.layers[0] as DataLayerConfig), seriesType: 'bar', isStacked: true }], }} /> ); @@ -1440,7 +1488,7 @@ describe('XYChart component', () => { {...defaultProps} args={{ ...args, - layers: [{ ...(args.layers[0] as DataLayerConfig), seriesType: 'area_stacked' }], + layers: [{ ...(args.layers[0] as DataLayerConfig), seriesType: 'area', isStacked: true }], }} /> ); @@ -1460,7 +1508,12 @@ describe('XYChart component', () => { args={{ ...args, layers: [ - { ...(args.layers[0] as DataLayerConfig), seriesType: 'bar_horizontal_stacked' }, + { + ...(args.layers[0] as DataLayerConfig), + seriesType: 'bar', + isStacked: true, + isHorizontal: true, + }, ], }} /> @@ -1487,7 +1540,8 @@ describe('XYChart component', () => { ...(args.layers[0] as DataLayerConfig), xAccessor: undefined, splitAccessor: 'e', - seriesType: 'bar_stacked', + seriesType: 'bar', + isStacked: true, }, ], }} @@ -1574,7 +1628,8 @@ describe('XYChart component', () => { layers: [ { ...(args.layers[0] as DataLayerConfig), - seriesType: 'bar_stacked', + seriesType: 'bar', + isStacked: true, isHistogram: true, }, ], @@ -1630,21 +1685,33 @@ describe('XYChart component', () => { { ...layer, accessors: ['a', 'b'], - yConfig: [ + decorations: [ { - type: 'yConfig', + type: 'dataDecorationConfig', forAccessor: 'a', - axisMode: 'left', + axisId: '1', }, { - type: 'yConfig', + type: 'dataDecorationConfig', forAccessor: 'b', - axisMode: 'right', + axisId: '2', }, ], table: dataWithoutFormats, }, ], + yAxisConfigs: [ + { + type: 'yAxisConfig', + id: '1', + position: 'left', + }, + { + type: 'yAxisConfig', + id: '2', + position: 'right', + }, + ], }; const component = getRenderedComponent(newArgs); @@ -1684,21 +1751,28 @@ describe('XYChart component', () => { { ...layer, accessors: ['c', 'd'], - yConfig: [ + decorations: [ { - type: 'yConfig', + type: 'dataDecorationConfig', forAccessor: 'c', - axisMode: 'left', + axisId: '1', }, { - type: 'yConfig', + type: 'dataDecorationConfig', forAccessor: 'd', - axisMode: 'left', + axisId: '1', }, ], table: dataWithoutFormats, }, ], + yAxisConfigs: [ + { + type: 'yAxisConfig', + id: '1', + position: 'left', + }, + ], }; const component = getRenderedComponent(newArgs); @@ -1720,14 +1794,14 @@ describe('XYChart component', () => { type: 'extendedDataLayer', accessors: ['a', 'b'], splitAccessor: undefined, - yConfig: [ + decorations: [ { - type: 'yConfig', + type: 'dataDecorationConfig', forAccessor: 'a', color: '#550000', }, { - type: 'yConfig', + type: 'dataDecorationConfig', forAccessor: 'b', color: '#FFFF00', }, @@ -1739,9 +1813,9 @@ describe('XYChart component', () => { type: 'extendedDataLayer', accessors: ['c'], splitAccessor: undefined, - yConfig: [ + decorations: [ { - type: 'yConfig', + type: 'dataDecorationConfig', forAccessor: 'c', color: '#FEECDF', }, @@ -1772,16 +1846,16 @@ describe('XYChart component', () => { }) ).toEqual('#FEECDF'); }); - test('color is not applied to chart when splitAccessor is defined or when yConfig is not configured', () => { + test('color is not applied to chart when splitAccessor is defined or when decorations is not configured', () => { const newArgs: XYProps = { ...args, layers: [ { ...layer, accessors: ['a'], - yConfig: [ + decorations: [ { - type: 'yConfig', + type: 'dataDecorationConfig', forAccessor: 'a', color: '#550000', }, @@ -2067,7 +2141,14 @@ describe('XYChart component', () => { {...defaultProps} args={{ ...args, - yLeftScale: 'sqrt', + yAxisConfigs: [ + { + type: 'yAxisConfig', + position: 'left', + showLabels: true, + scaleType: 'sqrt', + }, + ], }} /> ); @@ -2115,11 +2196,24 @@ describe('XYChart component', () => { test('it should set the tickLabel visibility on the x axis if the tick labels is hidden', () => { const { args } = sampleArgs(); - args.tickLabelsVisibilitySettings = { - x: false, - yLeft: true, - yRight: true, - type: 'tickLabelsConfig', + args.yAxisConfigs = [ + { + type: 'yAxisConfig', + position: 'left', + showLabels: true, + }, + { + type: 'yAxisConfig', + position: 'right', + showLabels: true, + }, + ]; + + args.xAxisConfig = { + type: 'xAxisConfig', + id: 'x', + showLabels: false, + position: 'bottom', }; const instance = shallow(); @@ -2136,12 +2230,18 @@ describe('XYChart component', () => { test('it should set the tickLabel visibility on the y axis if the tick labels is hidden', () => { const { args } = sampleArgs(); - args.tickLabelsVisibilitySettings = { - x: true, - yLeft: false, - yRight: false, - type: 'tickLabelsConfig', - }; + args.yAxisConfigs = [ + { + type: 'yAxisConfig', + position: 'left', + showLabels: false, + }, + { + type: 'yAxisConfig', + position: 'right', + showLabels: false, + }, + ]; const instance = shallow(); @@ -2157,11 +2257,24 @@ describe('XYChart component', () => { test('it should set the tickLabel visibility on the x axis if the tick labels is shown', () => { const { args } = sampleArgs(); - args.tickLabelsVisibilitySettings = { - x: true, - yLeft: true, - yRight: true, - type: 'tickLabelsConfig', + args.yAxisConfigs = [ + { + type: 'yAxisConfig', + position: 'left', + showLabels: true, + }, + { + type: 'yAxisConfig', + position: 'right', + showLabels: true, + }, + ]; + + args.xAxisConfig = { + type: 'xAxisConfig', + id: 'x', + showLabels: true, + position: 'bottom', }; const instance = shallow(); @@ -2178,11 +2291,25 @@ describe('XYChart component', () => { test('it should set the tickLabel orientation on the x axis', () => { const { args } = sampleArgs(); - args.labelsOrientation = { - x: -45, - yLeft: 0, - yRight: -90, - type: 'labelsOrientationConfig', + args.yAxisConfigs = [ + { + type: 'yAxisConfig', + position: 'left', + labelsOrientation: 0, + }, + { + type: 'yAxisConfig', + position: 'right', + labelsOrientation: -90, + }, + ]; + + args.xAxisConfig = { + type: 'xAxisConfig', + id: 'x', + showLabels: true, + labelsOrientation: -45, + position: 'bottom', }; const instance = shallow(); @@ -2199,11 +2326,24 @@ describe('XYChart component', () => { test('it should set the tickLabel visibility on the y axis if the tick labels is shown', () => { const { args } = sampleArgs(); - args.tickLabelsVisibilitySettings = { - x: false, - yLeft: true, - yRight: true, - type: 'tickLabelsConfig', + args.yAxisConfigs = [ + { + type: 'yAxisConfig', + position: 'left', + showLabels: true, + }, + { + type: 'yAxisConfig', + position: 'right', + showLabels: true, + }, + ]; + + args.xAxisConfig = { + type: 'xAxisConfig', + id: 'x', + showLabels: true, + position: 'bottom', }; const instance = shallow(); @@ -2220,11 +2360,25 @@ describe('XYChart component', () => { test('it should set the tickLabel orientation on the y axis', () => { const { args } = sampleArgs(); - args.labelsOrientation = { - x: -45, - yLeft: -90, - yRight: -90, - type: 'labelsOrientationConfig', + args.yAxisConfigs = [ + { + type: 'yAxisConfig', + position: 'left', + labelsOrientation: -90, + }, + { + type: 'yAxisConfig', + position: 'right', + labelsOrientation: -90, + }, + ]; + + args.xAxisConfig = { + type: 'xAxisConfig', + id: 'x', + showLabels: true, + labelsOrientation: -45, + position: 'bottom', }; const instance = shallow(); @@ -2266,43 +2420,47 @@ describe('XYChart component', () => { }; const args: XYProps = { - xTitle: '', - yTitle: '', - yRightTitle: '', - yLeftScale: 'linear', - yRightScale: 'linear', showTooltip: true, legend: { type: 'legendConfig', isVisible: false, position: Position.Top }, valueLabels: 'hide', - tickLabelsVisibilitySettings: { - type: 'tickLabelsConfig', - x: true, - yLeft: true, - yRight: true, - }, - gridlinesVisibilitySettings: { - type: 'gridlinesConfig', - x: true, - yLeft: false, - yRight: false, - }, - labelsOrientation: { - type: 'labelsOrientationConfig', - x: 0, - yLeft: 0, - yRight: 0, - }, - xExtent: { - mode: 'dataBounds', - type: 'axisExtentConfig', - }, - yLeftExtent: { - mode: 'full', - type: 'axisExtentConfig', - }, - yRightExtent: { - mode: 'full', - type: 'axisExtentConfig', + yAxisConfigs: [ + { + type: 'yAxisConfig', + position: 'left', + labelsOrientation: 0, + showGridLines: false, + showLabels: true, + title: '', + extent: { + mode: 'full', + type: 'axisExtentConfig', + }, + }, + { + type: 'yAxisConfig', + position: 'right', + labelsOrientation: 0, + showGridLines: false, + showLabels: true, + title: '', + extent: { + mode: 'full', + type: 'axisExtentConfig', + }, + }, + ], + xAxisConfig: { + type: 'xAxisConfig', + id: 'x', + title: '', + showLabels: true, + showGridLines: true, + labelsOrientation: 0, + position: 'bottom', + extent: { + mode: 'dataBounds', + type: 'axisExtentConfig', + }, }, markSizeRatio: 1, layers: [ @@ -2311,6 +2469,8 @@ describe('XYChart component', () => { type: 'dataLayer', layerType: LayerTypes.DATA, seriesType: 'line', + isStacked: false, + isHorizontal: false, showLines: true, xAccessor: 'a', accessors: ['c'], @@ -2318,6 +2478,7 @@ describe('XYChart component', () => { columnToLabel: '', xScaleType: 'ordinal', isHistogram: false, + isPercentage: false, palette: mockPaletteOutput, table: data1, }, @@ -2333,6 +2494,9 @@ describe('XYChart component', () => { columnToLabel: '', xScaleType: 'ordinal', isHistogram: false, + isStacked: false, + isHorizontal: false, + isPercentage: false, palette: mockPaletteOutput, table: data2, }, @@ -2363,45 +2527,51 @@ describe('XYChart component', () => { }; const args: XYProps = { - xTitle: '', - yTitle: '', - yRightTitle: '', legend: { type: 'legendConfig', isVisible: false, position: Position.Top }, valueLabels: 'hide', - showTooltip: true, - tickLabelsVisibilitySettings: { - type: 'tickLabelsConfig', - x: true, - yLeft: false, - yRight: false, - }, - gridlinesVisibilitySettings: { - type: 'gridlinesConfig', - x: true, - yLeft: false, - yRight: false, - }, - labelsOrientation: { - type: 'labelsOrientationConfig', - x: 0, - yLeft: 0, - yRight: 0, - }, - xExtent: { - mode: 'dataBounds', - type: 'axisExtentConfig', - }, - yLeftExtent: { - mode: 'full', - type: 'axisExtentConfig', - }, - yRightExtent: { - mode: 'full', - type: 'axisExtentConfig', + yAxisConfigs: [ + { + type: 'yAxisConfig', + position: 'left', + labelsOrientation: 0, + showGridLines: false, + showLabels: false, + title: '', + scaleType: 'linear', + extent: { + mode: 'full', + type: 'axisExtentConfig', + }, + }, + { + type: 'yAxisConfig', + position: 'right', + labelsOrientation: 0, + showGridLines: false, + showLabels: false, + scaleType: 'linear', + title: '', + extent: { + mode: 'full', + type: 'axisExtentConfig', + }, + }, + ], + xAxisConfig: { + type: 'xAxisConfig', + id: 'x', + title: '', + showLabels: true, + showGridLines: true, + labelsOrientation: 0, + position: 'bottom', + extent: { + mode: 'dataBounds', + type: 'axisExtentConfig', + }, }, + showTooltip: true, markSizeRatio: 1, - yLeftScale: 'linear', - yRightScale: 'linear', layers: [ { layerId: 'first', @@ -2415,6 +2585,9 @@ describe('XYChart component', () => { columnToLabel: '', xScaleType: 'ordinal', isHistogram: false, + isStacked: false, + isHorizontal: false, + isPercentage: false, palette: mockPaletteOutput, table: data, }, @@ -2443,45 +2616,51 @@ describe('XYChart component', () => { }; const args: XYProps = { - xTitle: '', - yTitle: '', - yRightTitle: '', showTooltip: true, legend: { type: 'legendConfig', isVisible: true, position: Position.Top }, valueLabels: 'hide', - tickLabelsVisibilitySettings: { - type: 'tickLabelsConfig', - x: true, - yLeft: false, - yRight: false, - }, - gridlinesVisibilitySettings: { - type: 'gridlinesConfig', - x: true, - yLeft: false, - yRight: false, - }, - labelsOrientation: { - type: 'labelsOrientationConfig', - x: 0, - yLeft: 0, - yRight: 0, - }, - xExtent: { - mode: 'dataBounds', - type: 'axisExtentConfig', - }, - yLeftExtent: { - mode: 'full', - type: 'axisExtentConfig', - }, - yRightExtent: { - mode: 'full', - type: 'axisExtentConfig', + yAxisConfigs: [ + { + type: 'yAxisConfig', + position: 'left', + labelsOrientation: 0, + showGridLines: false, + showLabels: false, + title: '', + scaleType: 'linear', + extent: { + mode: 'full', + type: 'axisExtentConfig', + }, + }, + { + type: 'yAxisConfig', + position: 'right', + labelsOrientation: 0, + showGridLines: false, + showLabels: false, + scaleType: 'linear', + title: '', + extent: { + mode: 'full', + type: 'axisExtentConfig', + }, + }, + ], + xAxisConfig: { + type: 'xAxisConfig', + id: 'x', + showLabels: true, + showGridLines: true, + labelsOrientation: 0, + title: '', + position: 'bottom', + extent: { + mode: 'dataBounds', + type: 'axisExtentConfig', + }, }, markSizeRatio: 1, - yLeftScale: 'linear', - yRightScale: 'linear', layers: [ { layerId: 'first', @@ -2495,6 +2674,9 @@ describe('XYChart component', () => { columnToLabel: '', xScaleType: 'ordinal', isHistogram: false, + isStacked: false, + isHorizontal: false, + isPercentage: false, palette: mockPaletteOutput, table: data, }, @@ -2631,7 +2813,7 @@ describe('XYChart component', () => { { ...sampleLayer, accessors: ['a'], table: data }, { ...sampleLayer, seriesType: 'bar', accessors: ['a'], table: data }, { ...sampleLayer, seriesType: 'area', accessors: ['a'], table: data }, - { ...sampleLayer, seriesType: 'area_stacked', accessors: ['a'], table: data }, + { ...sampleLayer, seriesType: 'area', isStacked: true, accessors: ['a'], table: data }, ]); const component = shallow( @@ -2661,7 +2843,13 @@ describe('XYChart component', () => { test('it should apply the xTitle if is specified', () => { const { args } = sampleArgs(); - args.xTitle = 'My custom x-axis title'; + args.xAxisConfig = { + type: 'xAxisConfig', + id: 'x', + showLabels: true, + title: 'My custom x-axis title', + position: 'bottom', + }; const component = shallow(); @@ -2671,11 +2859,26 @@ describe('XYChart component', () => { test('it should hide the X axis title if the corresponding switch is off', () => { const { args } = sampleArgs(); - args.axisTitlesVisibilitySettings = { - x: false, - yLeft: true, - yRight: true, - type: 'axisTitlesVisibilityConfig', + args.yAxisConfigs = [ + { + type: 'yAxisConfig', + position: 'left', + showTitle: true, + }, + { + type: 'yAxisConfig', + position: 'right', + showTitle: true, + }, + ]; + + args.xAxisConfig = { + type: 'xAxisConfig', + id: 'x', + showLabels: true, + showTitle: false, + title: 'My custom x-axis title', + position: 'bottom', }; const component = shallow(); @@ -2692,11 +2895,26 @@ describe('XYChart component', () => { test('it should show the X axis gridlines if the setting is on', () => { const { args } = sampleArgs(); - args.gridlinesVisibilitySettings = { - x: true, - yLeft: false, - yRight: false, - type: 'gridlinesConfig', + args.yAxisConfigs = [ + { + type: 'yAxisConfig', + position: 'left', + showGridLines: false, + }, + { + type: 'yAxisConfig', + position: 'right', + showGridLines: false, + }, + ]; + + args.xAxisConfig = { + type: 'xAxisConfig', + id: 'x', + showLabels: true, + showGridLines: true, + title: 'My custom x-axis title', + position: 'bottom', }; const component = shallow(); @@ -2741,10 +2959,13 @@ describe('XYChart component', () => { layerType: LayerTypes.DATA, showLines: true, seriesType: 'line', + isStacked: false, + isHorizontal: false, xAccessor: 'c', accessors: ['a', 'b'], xScaleType: 'ordinal', isHistogram: false, + isPercentage: false, palette: mockPaletteOutput, table: data, }; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx index 665bef44e539c..17c35ca7b56fe 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -47,20 +47,21 @@ import type { FilterEvent, BrushEvent, FormatFactory } from '../types'; import { isTimeChart } from '../../common/helpers'; import type { CommonXYDataLayerConfig, - ExtendedYConfig, - ReferenceLineYConfig, - SeriesType, + ReferenceLineDecorationConfig, + ExtendedReferenceLineDecorationConfig, XYChartProps, + AxisExtentConfigResult, } from '../../common/types'; import { isHorizontalChart, getAnnotationsLayers, getDataLayers, - Series, + AxisConfiguration, + getAxisPosition, getFormattedTablesByLayers, getLayersFormats, getLayersTitles, - isReferenceLineYConfig, + isReferenceLineDecorationConfig, getFilteredLayers, getReferenceLayers, isDataLayer, @@ -68,10 +69,16 @@ import { GroupsConfiguration, getLinesCausedPaddings, validateExtent, + Series, + getOriginalAxisPosition, } from '../helpers'; import { getXDomain, XyEndzones } from './x_domain'; import { getLegendAction } from './legend_action'; -import { ReferenceLines, computeChartMargins } from './reference_lines'; +import { + ReferenceLines, + computeChartMargins, + getAxisGroupForReferenceLine, +} from './reference_lines'; import { visualizationDefinitions } from '../definitions'; import { CommonXYLayerConfig } from '../../common/types'; import { SplitChart } from './split_chart'; @@ -138,8 +145,16 @@ function getValueLabelsStyling(isHorizontal: boolean): { }; } -function getIconForSeriesType(seriesType: SeriesType): IconType { - return visualizationDefinitions.find((c) => c.id === seriesType)!.icon || 'empty'; +function getIconForSeriesType(layer: CommonXYDataLayerConfig): IconType { + return ( + visualizationDefinitions.find( + (c) => + c.id === + `${layer.seriesType}${layer.isHorizontal ? '_horizontal' : ''}${ + layer.isPercentage ? '_percentage' : '' + }${layer.isStacked ? '_stacked' : ''}` + )!.icon || 'empty' + ); } export const XYChartReportable = React.memo(XYChart); @@ -166,15 +181,11 @@ export function XYChart({ fittingFunction, endValue, emphasizeFitting, - gridlinesVisibilitySettings, valueLabels, hideEndzones, - xExtent, - yLeftExtent, - yRightExtent, valuesInLegend, - yLeftScale, - yRightScale, + yAxisConfigs, + xAxisConfig, splitColumnAccessor, splitRowAccessor, } = args; @@ -204,9 +215,7 @@ export function XYChart({ ); if (dataLayers.length === 0) { - const icon: IconType = getIconForSeriesType( - getDataLayers(layers)?.[0]?.seriesType || SeriesTypes.BAR - ); + const icon: IconType = getIconForSeriesType(getDataLayers(layers)?.[0]); return ; } @@ -236,37 +245,35 @@ export function XYChart({ shouldRotate, formatFactory, fieldFormats, - yLeftScale, - yRightScale + yAxisConfigs + ); + + const axesConfiguration = getAxesConfiguration( + dataLayers, + shouldRotate, + formatFactory, + fieldFormats, + [...(yAxisConfigs ?? []), ...(xAxisConfig ? [xAxisConfig] : [])] ); - const xTitle = args.xTitle || (xAxisColumn && xAxisColumn.name); + const xTitle = xAxisConfig?.title || (xAxisColumn && xAxisColumn.name); const yAxesMap = { - left: yAxesConfiguration.find(({ groupId }) => groupId === 'left'), - right: yAxesConfiguration.find(({ groupId }) => groupId === 'right'), + left: yAxesConfiguration.find( + ({ position }) => position === getAxisPosition(Position.Left, shouldRotate) + ), + right: yAxesConfiguration.find( + ({ position }) => position === getAxisPosition(Position.Right, shouldRotate) + ), }; const titles = getLayersTitles( dataLayers, { splitColumnAccessor, splitRowAccessor }, - { xTitle: args.xTitle, yTitle: args.yTitle, yRightTitle: args.yRightTitle }, + { xTitle }, yAxesConfiguration ); - const axisTitlesVisibilitySettings = args.axisTitlesVisibilitySettings || { - x: true, - yLeft: true, - yRight: true, - }; - const tickLabelsVisibilitySettings = args.tickLabelsVisibilitySettings || { - x: true, - yLeft: true, - yRight: true, - }; - - const labelsOrientation = args.labelsOrientation || { x: 0, yLeft: 0, yRight: 0 }; - - const filteredBarLayers = dataLayers.filter((layer) => layer.seriesType.includes('bar')); + const filteredBarLayers = dataLayers.filter(({ seriesType }) => seriesType === SeriesTypes.BAR); const chartHasMoreThanOneBarSeries = filteredBarLayers.length > 1 || @@ -285,9 +292,18 @@ export function XYChart({ minInterval, isTimeViz, isHistogramViz, - xExtent + xAxisConfig?.extent ); + const axisTitlesVisibilitySettings = { + yLeft: yAxesMap?.left?.showTitle ?? true, + yRight: yAxesMap?.right?.showTitle ?? true, + }; + const tickLabelsVisibilitySettings = { + yLeft: yAxesMap?.left?.showLabels ?? true, + yRight: yAxesMap?.right?.showLabels ?? true, + }; + const getYAxesTitles = (axisSeries: Series[]) => { return axisSeries .map(({ layer, accessor }) => titles?.[layer]?.yTitles?.[accessor]) @@ -312,45 +328,48 @@ export function XYChart({ const rangeAnnotations = getRangeAnnotations(annotationsLayers); const visualConfigs = [ - ...referenceLineLayers.flatMap( - ({ yConfig }) => yConfig - ), + ...referenceLineLayers + .flatMap( + ({ decorations }) => decorations + ) + .map((config) => ({ + ...config, + position: config + ? getAxisGroupForReferenceLine(axesConfiguration, config, shouldRotate)?.position ?? + Position.Left + : Position.Bottom, + })), ...groupedLineAnnotations, ].filter(Boolean); const shouldHideDetails = annotationsLayers.length > 0 ? annotationsLayers[0].hide : false; - const linesPaddings = !shouldHideDetails ? getLinesCausedPaddings(visualConfigs, yAxesMap) : {}; + const linesPaddings = !shouldHideDetails + ? getLinesCausedPaddings(visualConfigs, yAxesMap, shouldRotate) + : {}; - const getYAxesStyle = (groupId: 'left' | 'right') => { - const tickVisible = - groupId === 'right' - ? tickLabelsVisibilitySettings?.yRight - : tickLabelsVisibilitySettings?.yLeft; + const getYAxesStyle = (axis: AxisConfiguration) => { + const tickVisible = axis.showLabels; + const position = getOriginalAxisPosition(axis.position, shouldRotate); const style = { tickLabel: { + fill: axis.labelColor, visible: tickVisible, - rotation: - groupId === 'right' - ? args.labelsOrientation?.yRight || 0 - : args.labelsOrientation?.yLeft || 0, + rotation: axis.labelsOrientation, padding: - linesPaddings[groupId] != null + linesPaddings[position] != null ? { - inner: linesPaddings[groupId], + inner: linesPaddings[position], } : undefined, }, axisTitle: { - visible: - groupId === 'right' - ? axisTitlesVisibilitySettings?.yRight - : axisTitlesVisibilitySettings?.yLeft, + visible: axis.showTitle, // if labels are not visible add the padding to the title padding: - !tickVisible && linesPaddings[groupId] != null + !tickVisible && linesPaddings[position] != null ? { - inner: linesPaddings[groupId], + inner: linesPaddings[position], } : undefined, }, @@ -359,7 +378,10 @@ export function XYChart({ }; const getYAxisDomain = (axis: GroupsConfiguration[number]) => { - const extent = axis.groupId === 'left' ? yLeftExtent : yRightExtent; + const extent: AxisExtentConfigResult = axis.extent || { + type: 'axisExtentConfig', + mode: 'full', + }; const hasBarOrArea = Boolean( axis.series.some((series) => { const layer = layersById[series.layer]; @@ -367,11 +389,12 @@ export function XYChart({ return false; } - return layer.seriesType.includes('bar') || layer.seriesType.includes('area'); + return layer.seriesType === SeriesTypes.BAR || layer.seriesType === SeriesTypes.AREA; }) ); const fit = !hasBarOrArea && extent.mode === AxisExtentModes.DATA_BOUNDS; + const padding = axis.boundsMargin || undefined; let min: number = NaN; let max: number = NaN; @@ -387,22 +410,31 @@ export function XYChart({ fit, min, max, + padding, includeDataFromIds: referenceLineLayers - .flatMap((l) => - l.yConfig ? l.yConfig.map((yConfig) => ({ layerId: l.layerId, yConfig })) : [] + .flatMap( + (l) => l.decorations?.map((decoration) => ({ layerId: l.layerId, decoration })) || [] ) - .filter(({ yConfig }) => yConfig.axisMode === axis.groupId) - .map(({ layerId, yConfig }) => - isReferenceLineYConfig(yConfig) - ? `${layerId}-${yConfig.value}-${yConfig.fill !== 'none' ? 'rect' : 'line'}` - : `${layerId}-${yConfig.forAccessor}-${yConfig.fill !== 'none' ? 'rect' : 'line'}` + .filter(({ decoration }) => { + if (decoration.axisId) { + return axis.groupId.includes(decoration.axisId); + } + + return ( + axis.position === getAxisPosition(decoration.position ?? Position.Left, shouldRotate) + ); + }) + .map(({ layerId, decoration }) => + isReferenceLineDecorationConfig(decoration) + ? `${layerId}-${decoration.value}-${decoration.fill !== 'none' ? 'rect' : 'line'}` + : `${layerId}-${decoration.forAccessor}-${decoration.fill !== 'none' ? 'rect' : 'line'}` ), }; }; const shouldShowValueLabels = // No stacked bar charts - dataLayers.every((layer) => !layer.seriesType.includes('stacked')) && + dataLayers.every((layer) => !layer.isStacked) && // No histogram charts !isHistogramViz; @@ -516,18 +548,17 @@ export function XYChart({ }; const isHistogramModeEnabled = dataLayers.some( - ({ isHistogram, seriesType }) => - isHistogram && - (seriesType.includes('stacked') || - !seriesType.includes('bar') || - !chartHasMoreThanOneBarSeries) + ({ isHistogram, seriesType, isStacked }) => + isHistogram && (isStacked || seriesType !== SeriesTypes.BAR || !chartHasMoreThanOneBarSeries) ); const shouldUseNewTimeAxis = isTimeViz && isHistogramModeEnabled && !useLegacyTimeAxis && !shouldRotate; + const defaultXAxisPosition = shouldRotate ? Position.Left : Position.Bottom; + const gridLineStyle = { - visible: gridlinesVisibilitySettings?.x, + visible: xAxisConfig?.showGridLines, strokeWidth: 1, }; const xAxisStyle: RecursivePartial = shouldUseNewTimeAxis @@ -535,26 +566,28 @@ export function XYChart({ ...MULTILAYER_TIME_AXIS_STYLE, tickLabel: { ...MULTILAYER_TIME_AXIS_STYLE.tickLabel, - visible: Boolean(tickLabelsVisibilitySettings?.x), + visible: Boolean(xAxisConfig?.showLabels), + fill: xAxisConfig?.labelColor, }, tickLine: { ...MULTILAYER_TIME_AXIS_STYLE.tickLine, - visible: Boolean(tickLabelsVisibilitySettings?.x), + visible: Boolean(xAxisConfig?.showLabels), }, axisTitle: { - visible: axisTitlesVisibilitySettings.x, + visible: xAxisConfig?.showTitle, }, } : { tickLabel: { - visible: tickLabelsVisibilitySettings?.x, - rotation: labelsOrientation?.x, + visible: xAxisConfig?.showLabels, + rotation: xAxisConfig?.labelsOrientation, padding: linesPaddings.bottom != null ? { inner: linesPaddings.bottom } : undefined, + fill: xAxisConfig?.labelColor, }, axisTitle: { - visible: axisTitlesVisibilitySettings.x, + visible: xAxisConfig?.showTitle, padding: - !tickLabelsVisibilitySettings?.x && linesPaddings.bottom != null + !xAxisConfig?.showLabels && linesPaddings.bottom != null ? { inner: linesPaddings.bottom } : undefined, }, @@ -610,8 +643,8 @@ export function XYChart({ ...chartTheme.chartPaddings, ...computeChartMargins( linesPaddings, - tickLabelsVisibilitySettings, - axisTitlesVisibilitySettings, + { ...tickLabelsVisibilitySettings, x: xAxisConfig?.showLabels }, + { ...axisTitlesVisibilitySettings, x: xAxisConfig?.showTitle }, yAxesMap, shouldRotate ), @@ -678,12 +711,24 @@ export function XYChart({ safeXAccessorLabelRenderer(d)} + hide={xAxisConfig?.hide || dataLayers[0]?.hide || !dataLayers[0]?.xAccessor} + tickFormat={(d) => { + let value = safeXAccessorLabelRenderer(d) || ''; + if (xAxisConfig?.truncate && value.length > xAxisConfig.truncate) { + value = `${value.slice(0, xAxisConfig.truncate)}...`; + } + return value; + }} style={xAxisStyle} + showOverlappingLabels={xAxisConfig?.showOverlappingLabels} + showDuplicatedTicks={xAxisConfig?.showDuplicates} timeAxisLayerCount={shouldUseNewTimeAxis ? 3 : 0} /> {isSplitChart && splitTable && ( @@ -704,15 +749,20 @@ export function XYChart({ position={axis.position} title={getYAxesTitles(axis.series)} gridLine={{ - visible: - axis.groupId === 'right' - ? gridlinesVisibilitySettings?.yRight - : gridlinesVisibilitySettings?.yLeft, + visible: axis.showGridLines, + }} + hide={axis.hide || dataLayers[0]?.hide} + tickFormat={(d) => { + let value = axis.formatter?.convert(d) || ''; + if (axis.truncate && value.length > axis.truncate) { + value = `${value.slice(0, axis.truncate)}...`; + } + return value; }} - hide={dataLayers[0]?.hide} - tickFormat={(d) => axis.formatter?.convert(d) || ''} - style={getYAxesStyle(axis.groupId)} + style={getYAxesStyle(axis)} domain={getYAxisDomain(axis)} + showOverlappingLabels={axis.showOverlappingLabels} + showDuplicatedTicks={axis.showDuplicates} ticks={5} /> ); @@ -726,9 +776,9 @@ export function XYChart({ histogramMode={dataLayers.every( (layer) => layer.isHistogram && - (layer.seriesType.includes('stacked') || !layer.splitAccessor) && - (layer.seriesType.includes('stacked') || - !layer.seriesType.includes('bar') || + (layer.isStacked || !layer.splitAccessor) && + (layer.isStacked || + layer.seriesType !== SeriesTypes.BAR || !chartHasMoreThanOneBarSeries) )} /> @@ -758,18 +808,12 @@ export function XYChart({ {referenceLineLayers.length ? ( ) : null} {rangeAnnotations.length || groupedLineAnnotations.length ? ( diff --git a/src/plugins/chart_expressions/expression_xy/public/definitions/visualizations.ts b/src/plugins/chart_expressions/expression_xy/public/definitions/visualizations.ts index 9cd1540c7bbeb..d16d8204846fc 100644 --- a/src/plugins/chart_expressions/expression_xy/public/definitions/visualizations.ts +++ b/src/plugins/chart_expressions/expression_xy/public/definitions/visualizations.ts @@ -22,13 +22,13 @@ import { export const visualizationDefinitions = [ { id: SeriesTypes.BAR, icon: BarIcon }, - { id: SeriesTypes.BAR_STACKED, icon: BarStackedIcon }, - { id: SeriesTypes.BAR_HORIZONTAL, icon: BarHorizontalIcon }, - { id: SeriesTypes.BAR_PERCENTAGE_STACKED, icon: BarPercentageIcon }, - { id: SeriesTypes.BAR_HORIZONTAL_STACKED, icon: BarHorizontalStackedIcon }, - { id: SeriesTypes.BAR_HORIZONTAL_PERCENTAGE_STACKED, icon: BarHorizontalPercentageIcon }, + { id: `${SeriesTypes.BAR}_stacked`, icon: BarStackedIcon }, + { id: `${SeriesTypes.BAR}_horizontal`, icon: BarHorizontalIcon }, + { id: `${SeriesTypes.BAR}_percentage_stacked`, icon: BarPercentageIcon }, + { id: `${SeriesTypes.BAR}_horizontal_stacked`, icon: BarHorizontalStackedIcon }, + { id: `${SeriesTypes.BAR}_horizontal_percentage_stacked`, icon: BarHorizontalPercentageIcon }, { id: SeriesTypes.LINE, icon: LineIcon }, { id: SeriesTypes.AREA, icon: AreaIcon }, - { id: SeriesTypes.AREA_STACKED, icon: AreaStackedIcon }, - { id: SeriesTypes.AREA_PERCENTAGE_STACKED, icon: AreaPercentageIcon }, + { id: `${SeriesTypes.AREA}_stacked`, icon: AreaStackedIcon }, + { id: `${SeriesTypes.AREA}_percentage_stacked`, icon: AreaPercentageIcon }, ]; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx b/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx index d6746cafc0296..c4aebbfb96902 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/annotations.tsx @@ -11,31 +11,34 @@ import { EuiFlexGroup, EuiIcon, EuiIconProps, EuiText } from '@elastic/eui'; import classnames from 'classnames'; import type { IconPosition, - YAxisMode, - ExtendedYConfig, + ReferenceLineDecorationConfig, CollectiveConfig, } from '../../common/types'; import { getBaseIconPlacement } from '../components'; import { hasIcon, iconSet } from './icon'; +import { AxesMap, getOriginalAxisPosition } from './axes_configuration'; export const LINES_MARKER_SIZE = 20; -type PartialExtendedYConfig = Pick< - ExtendedYConfig, - 'axisMode' | 'icon' | 'iconPosition' | 'textVisibility' ->; +type PartialReferenceLineDecorationConfig = Pick< + ReferenceLineDecorationConfig, + 'icon' | 'iconPosition' | 'textVisibility' +> & { + position?: Position; +}; -type PartialCollectiveConfig = Pick; +type PartialCollectiveConfig = Pick; -const isExtendedYConfig = ( - config: PartialExtendedYConfig | PartialCollectiveConfig | undefined -): config is PartialExtendedYConfig => - (config as PartialExtendedYConfig)?.iconPosition ? true : false; +const isExtendedDecorationConfig = ( + config: PartialReferenceLineDecorationConfig | PartialCollectiveConfig | undefined +): config is PartialReferenceLineDecorationConfig => + (config as PartialReferenceLineDecorationConfig)?.iconPosition ? true : false; // Note: it does not take into consideration whether the reference line is in view or not export const getLinesCausedPaddings = ( - visualConfigs: Array, - axesMap: Record<'left' | 'right', unknown> + visualConfigs: Array, + axesMap: AxesMap, + shouldRotate: boolean ) => { // collect all paddings for the 4 axis: if any text is detected double it. const paddings: Partial> = {}; @@ -44,11 +47,15 @@ export const getLinesCausedPaddings = ( if (!config) { return; } - const { axisMode, icon, textVisibility } = config; - const iconPosition = isExtendedYConfig(config) ? config.iconPosition : undefined; + const { position, icon, textVisibility } = config; + const iconPosition = isExtendedDecorationConfig(config) ? config.iconPosition : undefined; - if (axisMode && (hasIcon(icon) || textVisibility)) { - const placement = getBaseIconPlacement(iconPosition, axesMap, axisMode); + if (position && (hasIcon(icon) || textVisibility)) { + const placement = getBaseIconPlacement( + iconPosition, + axesMap, + getOriginalAxisPosition(position, shouldRotate) + ); paddings[placement] = Math.max( paddings[placement] || 0, LINES_MARKER_SIZE * (textVisibility ? 2 : 1) // double the padding size if there's text @@ -174,7 +181,7 @@ export const AnnotationIcon = ({ }; interface MarkerConfig { - axisMode?: YAxisMode; + position?: Position; icon?: string; textVisibility?: boolean; iconPosition?: IconPosition; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.test.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.test.ts index 29f146bfa4f91..eb5e2a29c2cdc 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.test.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { DataLayerConfig } from '../../common'; +import { DataLayerConfig, YAxisConfigResult } from '../../common'; import { LayerTypes } from '../../common/constants'; import { Datatable } from '@kbn/expressions-plugin/public'; import { getAxesConfiguration } from './axes_configuration'; @@ -221,6 +221,14 @@ describe('axes_configuration', () => { }, }; + const yAxisConfigs: YAxisConfigResult[] = [ + { + id: '1', + position: 'right', + type: 'yAxisConfig', + }, + ]; + const sampleLayer: DataLayerConfig = { layerId: 'first', type: 'dataLayer', @@ -233,6 +241,9 @@ describe('axes_configuration', () => { columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', xScaleType: 'ordinal', isHistogram: false, + isPercentage: false, + isStacked: false, + isHorizontal: false, palette: { type: 'palette', name: 'default' }, table: tables.first, }; @@ -253,7 +264,7 @@ describe('axes_configuration', () => { it('should map auto series to left axis', () => { const formatFactory = jest.fn(); - const groups = getAxesConfiguration([sampleLayer], false, formatFactory, fieldFormats); + const groups = getAxesConfiguration([sampleLayer], false, formatFactory, fieldFormats, []); expect(groups.length).toEqual(1); expect(groups[0].position).toEqual('left'); expect(groups[0].series[0].accessor).toEqual('yAccessorId'); @@ -263,7 +274,7 @@ describe('axes_configuration', () => { it('should map auto series to right axis if formatters do not match', () => { const formatFactory = jest.fn(); const twoSeriesLayer = { ...sampleLayer, accessors: ['yAccessorId', 'yAccessorId2'] }; - const groups = getAxesConfiguration([twoSeriesLayer], false, formatFactory, fieldFormats); + const groups = getAxesConfiguration([twoSeriesLayer], false, formatFactory, fieldFormats, []); expect(groups.length).toEqual(2); expect(groups[0].position).toEqual('left'); expect(groups[1].position).toEqual('right'); @@ -277,7 +288,7 @@ describe('axes_configuration', () => { ...sampleLayer, accessors: ['yAccessorId', 'yAccessorId2', 'yAccessorId3'], }; - const groups = getAxesConfiguration([threeSeriesLayer], false, formatFactory, fieldFormats); + const groups = getAxesConfiguration([threeSeriesLayer], false, formatFactory, fieldFormats, []); expect(groups.length).toEqual(2); expect(groups[0].position).toEqual('left'); expect(groups[1].position).toEqual('right'); @@ -292,12 +303,13 @@ describe('axes_configuration', () => { [ { ...sampleLayer, - yConfig: [{ type: 'yConfig', forAccessor: 'yAccessorId', axisMode: 'right' }], + decorations: [{ type: 'dataDecorationConfig', forAccessor: 'yAccessorId', axisId: '1' }], }, ], false, formatFactory, - fieldFormats + fieldFormats, + yAxisConfigs ); expect(groups.length).toEqual(1); expect(groups[0].position).toEqual('right'); @@ -312,19 +324,20 @@ describe('axes_configuration', () => { { ...sampleLayer, accessors: ['yAccessorId', 'yAccessorId3', 'yAccessorId4'], - yConfig: [{ type: 'yConfig', forAccessor: 'yAccessorId', axisMode: 'right' }], + decorations: [{ type: 'dataDecorationConfig', forAccessor: 'yAccessorId', axisId: '1' }], }, ], false, formatFactory, - fieldFormats + fieldFormats, + yAxisConfigs ); expect(groups.length).toEqual(2); - expect(groups[0].position).toEqual('left'); - expect(groups[0].series[0].accessor).toEqual('yAccessorId3'); - expect(groups[0].series[1].accessor).toEqual('yAccessorId4'); - expect(groups[1].position).toEqual('right'); - expect(groups[1].series[0].accessor).toEqual('yAccessorId'); + expect(groups[0].position).toEqual('right'); + expect(groups[0].series[0].accessor).toEqual('yAccessorId'); + expect(groups[1].position).toEqual('left'); + expect(groups[1].series[0].accessor).toEqual('yAccessorId3'); + expect(groups[1].series[1].accessor).toEqual('yAccessorId4'); expect(formatFactory).toHaveBeenCalledWith({ id: 'number', params: {} }); expect(formatFactory).toHaveBeenCalledWith({ id: 'currency', params: {} }); }); @@ -336,12 +349,13 @@ describe('axes_configuration', () => { { ...sampleLayer, accessors: ['yAccessorId', 'yAccessorId3', 'yAccessorId4'], - yConfig: [{ type: 'yConfig', forAccessor: 'yAccessorId', axisMode: 'right' }], + decorations: [{ type: 'dataDecorationConfig', forAccessor: 'yAccessorId', axisId: '1' }], }, ], false, formatFactory, - fieldFormats + fieldFormats, + yAxisConfigs ); expect(formatFactory).toHaveBeenCalledTimes(2); expect(formatFactory).toHaveBeenCalledWith({ id: 'number', params: {} }); diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.ts index 415c240a725d1..f95423ea83854 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/axes_configuration.ts @@ -6,15 +6,18 @@ * Side Public License, v 1. */ +import { Position } from '@elastic/charts'; import type { IFieldFormat, SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; import { getAccessorByDimension } from '@kbn/visualizations-plugin/common/utils'; import { FormatFactory } from '../types'; import { AxisExtentConfig, CommonXYDataLayerConfig, - ExtendedYConfig, - YConfig, - YScaleType, + DataDecorationConfig, + YAxisConfig, + ReferenceLineDecorationConfig, + YAxisConfigResult, + XAxisConfigResult, } from '../../common'; import { LayersFieldFormats } from './layers'; @@ -25,15 +28,26 @@ export interface Series { interface FormattedMetric extends Series { fieldFormat: SerializedFieldFormat; + axisId?: string; } -export type GroupsConfiguration = Array<{ - groupId: 'left' | 'right'; - position: 'left' | 'right' | 'bottom' | 'top'; +interface AxesSeries { + [key: string]: FormattedMetric[]; +} + +export interface AxisConfiguration extends Omit { + /** + * Axis group identificator. Format: `axis-${axis.id}` or just `left`/`right`. + */ + groupId: string; + position: Position; formatter?: IFieldFormat; series: Series[]; - scale?: YScaleType; -}>; +} + +export type GroupsConfiguration = AxisConfiguration[]; + +export type AxesMap = Record<'left' | 'right', AxisConfiguration | undefined>; export function isFormatterCompatible( formatter1: SerializedFieldFormat, @@ -42,91 +56,203 @@ export function isFormatterCompatible( return formatter1?.id === formatter2?.id; } +const LEFT_GLOBAL_AXIS_ID = 'left'; +const RIGHT_GLOBAL_AXIS_ID = 'right'; + +function isAxisSeriesAppliedForFormatter( + series: FormattedMetric[], + currentSeries: FormattedMetric +) { + return series.every((leftSeries) => + isFormatterCompatible(leftSeries.fieldFormat, currentSeries.fieldFormat) + ); +} + export function groupAxesByType( layers: CommonXYDataLayerConfig[], - fieldFormats: LayersFieldFormats + fieldFormats: LayersFieldFormats, + yAxisConfigs?: YAxisConfig[] ) { - const series: { - auto: FormattedMetric[]; - left: FormattedMetric[]; - right: FormattedMetric[]; - bottom: FormattedMetric[]; - } = { + const series: AxesSeries = { auto: [], left: [], right: [], - bottom: [], }; + const leftSeriesKeys: string[] = []; + const rightSeriesKeys: string[] = []; + layers.forEach((layer) => { const { layerId, table } = layer; layer.accessors.forEach((accessor) => { - const yConfig: Array | undefined = layer.yConfig; + const dataDecorations: + | Array + | undefined = layer.decorations; const yAccessor = getAccessorByDimension(accessor, table.columns); - const mode = - yConfig?.find(({ forAccessor }) => forAccessor === yAccessor)?.axisMode || 'auto'; + const decorationByAccessor = dataDecorations?.find( + (decorationConfig) => decorationConfig.forAccessor === yAccessor + ); + const axisConfigById = yAxisConfigs?.find( + (axis) => + decorationByAccessor?.axisId && axis.id && axis.id === decorationByAccessor?.axisId + ); + const key = axisConfigById?.id ? `axis-${axisConfigById?.id}` : 'auto'; const fieldFormat = fieldFormats[layerId].yAccessors[yAccessor]!; - series[mode].push({ layer: layer.layerId, accessor: yAccessor, fieldFormat }); + if (!series[key]) { + series[key] = []; + } + series[key].push({ layer: layer.layerId, accessor: yAccessor, fieldFormat }); + + if (axisConfigById?.position === Position.Left) { + leftSeriesKeys.push(key); + } else if (axisConfigById?.position === Position.Right) { + rightSeriesKeys.push(key); + } }); }); const tablesExist = layers.filter(({ table }) => Boolean(table)).length > 0; + leftSeriesKeys.push(LEFT_GLOBAL_AXIS_ID); + rightSeriesKeys.push(RIGHT_GLOBAL_AXIS_ID); + series.auto.forEach((currentSeries) => { - if ( - series.left.length === 0 || - (tablesExist && - series.left.every((leftSeries) => - isFormatterCompatible(leftSeries.fieldFormat, currentSeries.fieldFormat) - )) - ) { - series.left.push(currentSeries); - } else if ( - series.right.length === 0 || - (tablesExist && - series.left.every((leftSeries) => - isFormatterCompatible(leftSeries.fieldFormat, currentSeries.fieldFormat) - )) - ) { - series.right.push(currentSeries); - } else if (series.right.length >= series.left.length) { - series.left.push(currentSeries); + const leftAxisGroupId = tablesExist + ? leftSeriesKeys.find((leftSeriesKey) => + isAxisSeriesAppliedForFormatter(series[leftSeriesKey], currentSeries) + ) + : undefined; + + const rightAxisGroupId = tablesExist + ? rightSeriesKeys.find((rightSeriesKey) => + isAxisSeriesAppliedForFormatter(series[rightSeriesKey], currentSeries) + ) + : undefined; + + let axisGroupId = LEFT_GLOBAL_AXIS_ID; + + if (series[LEFT_GLOBAL_AXIS_ID].length === 0 || leftAxisGroupId) { + axisGroupId = leftAxisGroupId || LEFT_GLOBAL_AXIS_ID; + } else if (series[RIGHT_GLOBAL_AXIS_ID].length === 0 || rightAxisGroupId) { + axisGroupId = rightAxisGroupId || RIGHT_GLOBAL_AXIS_ID; + } else if (series[RIGHT_GLOBAL_AXIS_ID].length >= series[LEFT_GLOBAL_AXIS_ID].length) { + axisGroupId = LEFT_GLOBAL_AXIS_ID; } else { - series.right.push(currentSeries); + axisGroupId = RIGHT_GLOBAL_AXIS_ID; } + + series[axisGroupId].push(currentSeries); }); + return series; } +export function getAxisPosition(position: Position, shouldRotate: boolean) { + if (shouldRotate) { + switch (position) { + case Position.Bottom: { + return Position.Right; + } + case Position.Right: { + return Position.Top; + } + case Position.Top: { + return Position.Left; + } + case Position.Left: { + return Position.Bottom; + } + } + } + + return position; +} + +export function getOriginalAxisPosition(position: Position, shouldRotate: boolean) { + if (shouldRotate) { + switch (position) { + case Position.Bottom: { + return Position.Left; + } + case Position.Right: { + return Position.Bottom; + } + case Position.Top: { + return Position.Right; + } + case Position.Left: { + return Position.Top; + } + } + } + + return position; +} + +function axisGlobalConfig(position: Position, yAxisConfigs?: YAxisConfig[]) { + return yAxisConfigs?.find((axis) => !axis.id && axis.position === position) || {}; +} + +const getXAxisConfig = (axisConfigs: Array = []) => + axisConfigs.find(({ type }) => type === 'xAxisConfig'); + export function getAxesConfiguration( layers: CommonXYDataLayerConfig[], shouldRotate: boolean, formatFactory: FormatFactory | undefined, fieldFormats: LayersFieldFormats, - yLeftScale?: YScaleType, - yRightScale?: YScaleType + axisConfigs?: Array ): GroupsConfiguration { - const series = groupAxesByType(layers, fieldFormats); + const series = groupAxesByType(layers, fieldFormats, axisConfigs); const axisGroups: GroupsConfiguration = []; + let position: Position; + + axisConfigs?.forEach((axis) => { + const groupId = axis.id ? `axis-${axis.id}` : undefined; + if (groupId && series[groupId] && series[groupId].length > 0) { + position = getAxisPosition(axis.position || Position.Left, shouldRotate); + axisGroups.push({ + groupId, + formatter: formatFactory?.(series[groupId][0].fieldFormat), + series: series[groupId].map(({ fieldFormat, ...currentSeries }) => currentSeries), + ...axisGlobalConfig(axis.position || Position.Left, axisConfigs), + ...axis, + position, + }); + } + }); - if (series.left.length > 0) { + if (series[LEFT_GLOBAL_AXIS_ID].length > 0) { + position = shouldRotate ? Position.Bottom : Position.Left; axisGroups.push({ - groupId: 'left', - position: shouldRotate ? 'bottom' : 'left', + groupId: LEFT_GLOBAL_AXIS_ID, formatter: formatFactory?.(series.left[0].fieldFormat), series: series.left.map(({ fieldFormat, ...currentSeries }) => currentSeries), - scale: yLeftScale, + ...axisGlobalConfig(Position.Left, axisConfigs), + position, }); } - if (series.right.length > 0) { + if (series[RIGHT_GLOBAL_AXIS_ID].length > 0) { + position = shouldRotate ? Position.Top : Position.Right; axisGroups.push({ - groupId: 'right', - position: shouldRotate ? 'top' : 'right', + groupId: RIGHT_GLOBAL_AXIS_ID, formatter: formatFactory?.(series.right[0].fieldFormat), series: series.right.map(({ fieldFormat, ...currentSeries }) => currentSeries), - scale: yRightScale, + ...axisGlobalConfig(Position.Right, axisConfigs), + position, + }); + } + + const xAxisConfig = getXAxisConfig(axisConfigs); + if (xAxisConfig) { + position = getAxisPosition(xAxisConfig.position || Position.Bottom, shouldRotate); + axisGroups.push({ + groupId: 'bottom', + series: [], + ...xAxisConfig, + position, }); } diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.test.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.test.ts index a30ede24617c6..c1405a2639f5e 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.test.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/color_assignment.test.ts @@ -54,8 +54,11 @@ describe('color_assignment', () => { type: 'dataLayer', showLines: true, isHistogram: true, + isPercentage: false, xScaleType: 'linear', seriesType: 'bar', + isStacked: false, + isHorizontal: false, palette: { type: 'palette', name: 'palette1' }, layerType: LayerTypes.DATA, splitAccessor: 'split1', @@ -68,7 +71,10 @@ describe('color_assignment', () => { showLines: true, xScaleType: 'linear', isHistogram: true, + isPercentage: false, seriesType: 'bar', + isStacked: false, + isHorizontal: false, palette: { type: 'palette', name: 'palette2' }, layerType: LayerTypes.DATA, splitAccessor: 'split2', diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx index 722bd329644cc..151d56ed63060 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx @@ -27,6 +27,7 @@ import { import type { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common/expression_functions'; import { PaletteRegistry, SeriesLayer } from '@kbn/coloring'; import { CommonXYDataLayerConfig, XScaleType } from '../../common'; +import { AxisModes, SeriesTypes } from '../../common/constants'; import { FormatFactory } from '../types'; import { getSeriesColor } from './state'; import { ColorAssignments } from './color_assignment'; @@ -350,10 +351,14 @@ export const getSeriesProps: GetSeriesPropsFn = ({ formattedDatatableInfo, defaultXScaleType, }): SeriesSpec => { - const { table, markSizeAccessor } = layer; - const isStacked = layer.seriesType.includes('stacked'); - const isPercentage = layer.seriesType.includes('percentage'); - const isBarChart = layer.seriesType.includes('bar'); + const { table, isStacked, markSizeAccessor } = layer; + const isPercentage = layer.isPercentage; + let stackMode: StackMode | undefined = isPercentage ? AxisModes.PERCENTAGE : undefined; + if (yAxis?.mode) { + stackMode = yAxis?.mode === AxisModes.NORMAL ? undefined : yAxis?.mode; + } + const scaleType = yAxis?.scaleType || ScaleType.Linear; + const isBarChart = layer.seriesType === SeriesTypes.BAR; const xColumnId = layer.xAccessor && getAccessorByDimension(layer.xAccessor, table.columns); const splitColumnId = layer.splitAccessor && getAccessorByDimension(layer.splitAccessor, table.columns); @@ -413,9 +418,9 @@ export const getSeriesProps: GetSeriesPropsFn = ({ data: rows, xScaleType: xColumnId ? layer.xScaleType ?? defaultXScaleType : 'ordinal', yScaleType: - formatter?.id === 'bytes' && yAxis?.scale === ScaleType.Linear + formatter?.id === 'bytes' && scaleType === ScaleType.Linear ? ScaleType.LinearBinary - : yAxis?.scale || ScaleType.Linear, + : scaleType, color: (series) => getColor( series, @@ -431,7 +436,7 @@ export const getSeriesProps: GetSeriesPropsFn = ({ ), groupId: yAxis?.groupId, enableHistogramMode, - stackMode: isPercentage ? StackMode.Percentage : undefined, + stackMode, timeZone, areaSeriesStyle: { point: getPointConfig({ diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts index dd3969c1b6412..eba7cf7c8a9f3 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/layers.ts @@ -17,7 +17,6 @@ import { CommonXYDataLayerConfig, CommonXYLayerConfig, ReferenceLineLayerConfig, - SeriesType, } from '../../common/types'; import { GroupsConfiguration } from './axes_configuration'; import { getFormat } from './format'; @@ -110,7 +109,7 @@ const getAccessorWithFieldFormat = ( const getYAccessorWithFieldFormat = ( dimension: string | ExpressionValueVisDimension | undefined, columns: Datatable['columns'], - seriesType: SeriesType + isPercentage: boolean ) => { if (!dimension) { return {}; @@ -118,7 +117,7 @@ const getYAccessorWithFieldFormat = ( const accessor = getAccessorByDimension(dimension, columns); let format = getFormat(columns, dimension) ?? { id: 'number' }; - if (format?.id !== 'percent' && seriesType.includes('percentage')) { + if (format?.id !== 'percent' && isPercentage) { format = { id: 'percent', params: { pattern: '0.[00]%' } }; } @@ -126,7 +125,7 @@ const getYAccessorWithFieldFormat = ( }; export const getLayerFormats = ( - { xAccessor, accessors, splitAccessor, table, seriesType }: CommonXYDataLayerConfig, + { xAccessor, accessors, splitAccessor, table, isPercentage }: CommonXYDataLayerConfig, { splitColumnAccessor, splitRowAccessor }: SplitAccessors ): LayerFieldFormats => { const yAccessors: Array = accessors; @@ -135,7 +134,7 @@ export const getLayerFormats = ( yAccessors: yAccessors.reduce( (formatters, a) => ({ ...formatters, - ...getYAccessorWithFieldFormat(a, table.columns, seriesType), + ...getYAccessorWithFieldFormat(a, table.columns, isPercentage), }), {} ), @@ -160,28 +159,21 @@ export const getLayersFormats = ( const getTitleForYAccessor = ( layerId: string, yAccessor: string | ExpressionValueVisDimension, - { yTitle, yRightTitle }: Omit, groups: GroupsConfiguration, columns: Datatable['columns'] ) => { const column = getColumnByAccessor(yAccessor, columns); - const isRight = groups.some((group) => - group.series.some( - ({ accessor, layer }) => - accessor === yAccessor && layer === layerId && group.groupId === 'right' - ) + const axisGroup = groups.find((group) => + group.series.some(({ accessor, layer }) => accessor === yAccessor && layer === layerId) ); - if (isRight) { - return yRightTitle || column!.name; - } - return yTitle || column!.name; + return axisGroup?.title || column!.name; }; export const getLayerTitles = ( { xAccessor, accessors, splitAccessor, table, layerId }: CommonXYDataLayerConfig, { splitColumnAccessor, splitRowAccessor }: SplitAccessors, - { xTitle, yTitle, yRightTitle }: CustomTitles, + { xTitle }: CustomTitles, groups: GroupsConfiguration ): LayerAccessorsTitles => { const mapTitle = (dimension?: string | ExpressionValueVisDimension) => { @@ -194,13 +186,7 @@ export const getLayerTitles = ( }; const getYTitle = (accessor: string) => ({ - [accessor]: getTitleForYAccessor( - layerId, - accessor, - { yTitle, yRightTitle }, - groups, - table.columns - ), + [accessor]: getTitleForYAccessor(layerId, accessor, groups, table.columns), }); const xColumnId = xAccessor && getAccessorByDimension(xAccessor, table.columns); diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts index 900cba4784853..a705dab47dc43 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/state.ts @@ -6,23 +6,15 @@ * Side Public License, v 1. */ -import type { CommonXYLayerConfig, SeriesType, ExtendedYConfig, YConfig } from '../../common'; +import type { + CommonXYLayerConfig, + ReferenceLineDecorationConfig, + DataDecorationConfig, +} from '../../common'; import { getDataLayers, isAnnotationsLayer, isDataLayer, isReferenceLine } from './visualization'; -export function isHorizontalSeries(seriesType: SeriesType) { - return ( - seriesType === 'bar_horizontal' || - seriesType === 'bar_horizontal_stacked' || - seriesType === 'bar_horizontal_percentage_stacked' - ); -} - -export function isStackedChart(seriesType: SeriesType) { - return seriesType.includes('stacked'); -} - export function isHorizontalChart(layers: CommonXYLayerConfig[]) { - return getDataLayers(layers).every((l) => isHorizontalSeries(l.seriesType)); + return getDataLayers(layers).every((l) => l.isHorizontal); } export const getSeriesColor = (layer: CommonXYLayerConfig, accessor: string) => { @@ -33,6 +25,10 @@ export const getSeriesColor = (layer: CommonXYLayerConfig, accessor: string) => ) { return null; } - const yConfig: Array | undefined = layer?.yConfig; - return yConfig?.find((yConf) => yConf.forAccessor === accessor)?.color || null; + const decorations: Array | undefined = + layer?.decorations; + return ( + decorations?.find((decorationConfig) => decorationConfig.forAccessor === accessor)?.color || + null + ); }; diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts b/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts index 480fa5374238e..87d27d30badb2 100644 --- a/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts +++ b/src/plugins/chart_expressions/expression_xy/public/helpers/visualization.ts @@ -10,7 +10,7 @@ import { LayerTypes, REFERENCE_LINE, REFERENCE_LINE_LAYER, - REFERENCE_LINE_Y_CONFIG, + EXTENDED_REFERENCE_LINE_DECORATION_CONFIG, } from '../../common/constants'; import { CommonXYLayerConfig, @@ -19,8 +19,8 @@ import { CommonXYAnnotationLayerConfig, ReferenceLineLayerConfig, ReferenceLineConfig, - ExtendedYConfigResult, - ReferenceLineYConfig, + ReferenceLineDecorationConfigResult, + ExtendedReferenceLineDecorationConfig, } from '../../common/types'; export const isDataLayer = (layer: CommonXYLayerConfig): layer is CommonXYDataLayerConfig => @@ -35,9 +35,10 @@ export const isReferenceLayer = (layer: CommonXYLayerConfig): layer is Reference export const isReferenceLine = (layer: CommonXYLayerConfig): layer is ReferenceLineConfig => layer.type === REFERENCE_LINE; -export const isReferenceLineYConfig = ( - yConfig: ReferenceLineYConfig | ExtendedYConfigResult -): yConfig is ReferenceLineYConfig => yConfig.type === REFERENCE_LINE_Y_CONFIG; +export const isReferenceLineDecorationConfig = ( + decoration: ExtendedReferenceLineDecorationConfig | ReferenceLineDecorationConfigResult +): decoration is ExtendedReferenceLineDecorationConfig => + decoration.type === EXTENDED_REFERENCE_LINE_DECORATION_CONFIG; export const isReferenceLineOrLayer = ( layer: CommonXYLayerConfig diff --git a/src/plugins/chart_expressions/expression_xy/public/plugin.ts b/src/plugins/chart_expressions/expression_xy/public/plugin.ts index 0dc6f62df3183..f0cc90baab1e4 100755 --- a/src/plugins/chart_expressions/expression_xy/public/plugin.ts +++ b/src/plugins/chart_expressions/expression_xy/public/plugin.ts @@ -18,18 +18,16 @@ import { xyVisFunction, layeredXyVisFunction, extendedDataLayerFunction, + dataDecorationConfigFunction, + xAxisConfigFunction, yAxisConfigFunction, - extendedYAxisConfigFunction, legendConfigFunction, - gridlinesConfigFunction, axisExtentConfigFunction, - tickLabelsConfigFunction, referenceLineFunction, referenceLineLayerFunction, annotationLayerFunction, - labelsOrientationConfigFunction, - axisTitlesVisibilityConfigFunction, extendedAnnotationLayerFunction, + referenceLineDecorationConfigFunction, } from '../common/expression_functions'; import { GetStartDepsFn, getXyChartRenderer } from './expression_renderers'; @@ -55,18 +53,16 @@ export class ExpressionXyPlugin { { expressions, charts }: SetupDeps ): ExpressionXyPluginSetup { expressions.registerFunction(yAxisConfigFunction); - expressions.registerFunction(extendedYAxisConfigFunction); + expressions.registerFunction(dataDecorationConfigFunction); + expressions.registerFunction(referenceLineDecorationConfigFunction); expressions.registerFunction(legendConfigFunction); - expressions.registerFunction(gridlinesConfigFunction); expressions.registerFunction(extendedDataLayerFunction); expressions.registerFunction(axisExtentConfigFunction); - expressions.registerFunction(tickLabelsConfigFunction); + expressions.registerFunction(xAxisConfigFunction); expressions.registerFunction(annotationLayerFunction); expressions.registerFunction(extendedAnnotationLayerFunction); - expressions.registerFunction(labelsOrientationConfigFunction); expressions.registerFunction(referenceLineFunction); expressions.registerFunction(referenceLineLayerFunction); - expressions.registerFunction(axisTitlesVisibilityConfigFunction); expressions.registerFunction(xyVisFunction); expressions.registerFunction(layeredXyVisFunction); diff --git a/src/plugins/chart_expressions/expression_xy/server/plugin.ts b/src/plugins/chart_expressions/expression_xy/server/plugin.ts index 4ddac2b3a3f79..f0e0fc141302a 100755 --- a/src/plugins/chart_expressions/expression_xy/server/plugin.ts +++ b/src/plugins/chart_expressions/expression_xy/server/plugin.ts @@ -11,16 +11,14 @@ import { CoreSetup, CoreStart, Plugin } from '@kbn/core/server'; import { ExpressionXyPluginSetup, ExpressionXyPluginStart } from './types'; import { xyVisFunction, - yAxisConfigFunction, - extendedYAxisConfigFunction, legendConfigFunction, - gridlinesConfigFunction, + dataDecorationConfigFunction, + xAxisConfigFunction, + yAxisConfigFunction, + referenceLineDecorationConfigFunction, axisExtentConfigFunction, - tickLabelsConfigFunction, annotationLayerFunction, - labelsOrientationConfigFunction, referenceLineFunction, - axisTitlesVisibilityConfigFunction, extendedDataLayerFunction, referenceLineLayerFunction, layeredXyVisFunction, @@ -33,18 +31,16 @@ export class ExpressionXyPlugin { public setup(core: CoreSetup, { expressions }: SetupDeps) { expressions.registerFunction(yAxisConfigFunction); - expressions.registerFunction(extendedYAxisConfigFunction); + expressions.registerFunction(dataDecorationConfigFunction); + expressions.registerFunction(xAxisConfigFunction); + expressions.registerFunction(referenceLineDecorationConfigFunction); expressions.registerFunction(legendConfigFunction); - expressions.registerFunction(gridlinesConfigFunction); expressions.registerFunction(extendedDataLayerFunction); expressions.registerFunction(axisExtentConfigFunction); - expressions.registerFunction(tickLabelsConfigFunction); expressions.registerFunction(annotationLayerFunction); expressions.registerFunction(extendedAnnotationLayerFunction); - expressions.registerFunction(labelsOrientationConfigFunction); expressions.registerFunction(referenceLineFunction); expressions.registerFunction(referenceLineLayerFunction); - expressions.registerFunction(axisTitlesVisibilityConfigFunction); expressions.registerFunction(xyVisFunction); expressions.registerFunction(layeredXyVisFunction); } diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts index 405563a0a0ce2..715df94d82ac4 100644 --- a/x-pack/plugins/lens/public/index.ts +++ b/x-pack/plugins/lens/public/index.ts @@ -18,6 +18,9 @@ export type { ValidLayer, XYDataLayerConfig, XYAnnotationLayerConfig, + YAxisMode, + SeriesType, + YConfig, } from './xy_visualization/types'; export type { DatasourcePublicAPI, @@ -74,13 +77,10 @@ export type { } from './indexpattern_datasource/types'; export type { XYArgs, - ExtendedYConfig, XYRender, LayerType, - YAxisMode, LineStyle, FillStyle, - SeriesType, YScaleType, XScaleType, AxisConfig, @@ -88,7 +88,6 @@ export type { XYChartProps, LegendConfig, IconPosition, - ExtendedYConfigResult, DataLayerArgs, ValueLabelMode, AxisExtentMode, @@ -97,14 +96,9 @@ export type { AxisExtentConfig, LegendConfigResult, AxesSettingsConfig, - GridlinesConfigResult, - TickLabelsConfigResult, AxisExtentConfigResult, ReferenceLineLayerArgs, - LabelsOrientationConfig, ReferenceLineLayerConfig, - LabelsOrientationConfigResult, - AxisTitlesVisibilityConfigResult, } from '@kbn/expression-xy-plugin/common'; export type { LensEmbeddableInput, LensSavedObjectAttributes, Embeddable } from './embeddable'; diff --git a/x-pack/plugins/lens/public/shared_components/axis_title_settings.tsx b/x-pack/plugins/lens/public/shared_components/axis_title_settings.tsx index 86a9caba2c2f4..5c1cc704671be 100644 --- a/x-pack/plugins/lens/public/shared_components/axis_title_settings.tsx +++ b/x-pack/plugins/lens/public/shared_components/axis_title_settings.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useMemo } from 'react'; import { EuiSpacer, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AxesSettingsConfig } from '@kbn/expression-xy-plugin/common'; +import { AxesSettingsConfig } from '../xy_visualization/types'; import { LabelMode, useDebouncedValue, VisLabel } from '.'; type AxesSettingsConfigKeys = keyof AxesSettingsConfig; diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap index 5ccb948dd88f9..548274f463323 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap @@ -5,28 +5,6 @@ Object { "chain": Array [ Object { "arguments": Object { - "axisTitlesVisibilitySettings": Array [ - Object { - "chain": Array [ - Object { - "arguments": Object { - "x": Array [ - true, - ], - "yLeft": Array [ - true, - ], - "yRight": Array [ - true, - ], - }, - "function": "axisTitlesVisibilityConfig", - "type": "function", - }, - ], - "type": "expression", - }, - ], "curveType": Array [ "LINEAR", ], @@ -42,53 +20,9 @@ Object { "fittingFunction": Array [ "Carry", ], - "gridlinesVisibilitySettings": Array [ - Object { - "chain": Array [ - Object { - "arguments": Object { - "x": Array [ - false, - ], - "yLeft": Array [ - true, - ], - "yRight": Array [ - true, - ], - }, - "function": "gridlinesConfig", - "type": "function", - }, - ], - "type": "expression", - }, - ], "hideEndzones": Array [ true, ], - "labelsOrientation": Array [ - Object { - "chain": Array [ - Object { - "arguments": Object { - "x": Array [ - 0, - ], - "yLeft": Array [ - -90, - ], - "yRight": Array [ - -45, - ], - }, - "function": "labelsOrientationConfig", - "type": "function", - }, - ], - "type": "expression", - }, - ], "layers": Array [ Object { "chain": Array [ @@ -101,12 +35,16 @@ Object { "columnToLabel": Array [ "{\\"b\\":\\"col_b\\",\\"c\\":\\"col_c\\",\\"d\\":\\"col_d\\"}", ], + "decorations": Array [], "hide": Array [ false, ], "isHistogram": Array [ false, ], + "isHorizontal": Array [], + "isPercentage": Array [], + "isStacked": Array [], "layerId": Array [ "first", ], @@ -144,7 +82,6 @@ Object { "xScaleType": Array [ "linear", ], - "yConfig": Array [], }, "function": "extendedDataLayer", "type": "function", @@ -182,103 +119,138 @@ Object { "type": "expression", }, ], - "tickLabelsVisibilitySettings": Array [ - Object { - "chain": Array [ - Object { - "arguments": Object { - "x": Array [ - false, - ], - "yLeft": Array [ - true, - ], - "yRight": Array [ - true, - ], - }, - "function": "tickLabelsConfig", - "type": "function", - }, - ], - "type": "expression", - }, - ], "valueLabels": Array [ "hide", ], "valuesInLegend": Array [ false, ], - "xExtent": Array [ + "xAxisConfig": Array [ Object { "chain": Array [ Object { "arguments": Object { - "lowerBound": Array [], - "mode": Array [], - "upperBound": Array [], + "extent": Array [], + "id": Array [ + "x", + ], + "labelsOrientation": Array [ + 0, + ], + "position": Array [ + "bottom", + ], + "showGridLines": Array [ + false, + ], + "showLabels": Array [ + false, + ], + "showTitle": Array [ + true, + ], + "title": Array [ + "", + ], }, - "function": "axisExtentConfig", + "function": "xAxisConfig", "type": "function", }, ], "type": "expression", }, ], - "xTitle": Array [ - "", - ], - "yLeftExtent": Array [ + "yAxisConfigs": Array [ Object { "chain": Array [ Object { "arguments": Object { - "lowerBound": Array [], - "mode": Array [], - "upperBound": Array [], + "extent": Array [], + "id": Array [], + "labelsOrientation": Array [ + -90, + ], + "position": Array [ + "left", + ], + "scaleType": Array [ + "linear", + ], + "showGridLines": Array [ + true, + ], + "showLabels": Array [ + true, + ], + "showTitle": Array [ + true, + ], + "title": Array [ + "", + ], }, - "function": "axisExtentConfig", + "function": "yAxisConfig", "type": "function", }, ], "type": "expression", }, - ], - "yLeftScale": Array [ - "linear", - ], - "yRightExtent": Array [ Object { "chain": Array [ Object { "arguments": Object { - "lowerBound": Array [ - 123, + "extent": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "lowerBound": Array [ + 123, + ], + "mode": Array [ + "custom", + ], + "upperBound": Array [ + 456, + ], + }, + "function": "axisExtentConfig", + "type": "function", + }, + ], + "type": "expression", + }, + ], + "id": Array [], + "labelsOrientation": Array [ + -45, + ], + "position": Array [ + "right", ], - "mode": Array [ - "custom", + "scaleType": Array [ + "linear", ], - "upperBound": Array [ - 456, + "showGridLines": Array [ + true, + ], + "showLabels": Array [ + true, + ], + "showTitle": Array [ + true, + ], + "title": Array [ + "", ], }, - "function": "axisExtentConfig", + "function": "yAxisConfig", "type": "function", }, ], "type": "expression", }, ], - "yRightScale": Array [ - "linear", - ], - "yRightTitle": Array [ - "", - ], - "yTitle": Array [ - "", - ], }, "function": "layeredXyVis", "type": "function", diff --git a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx index ece9a6d28893e..193a91523ecc8 100644 --- a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx @@ -7,13 +7,18 @@ import { groupBy, partition } from 'lodash'; import { i18n } from '@kbn/i18n'; -import type { YAxisMode, ExtendedYConfig } from '@kbn/expression-xy-plugin/common'; import { Datatable } from '@kbn/expressions-plugin/public'; import { layerTypes } from '../../common'; import type { DatasourceLayers, FramePublicAPI, Visualization } from '../types'; import { groupAxesByType } from './axes_configuration'; import { isHorizontalChart, isPercentageSeries, isStackedChart } from './state_helpers'; -import type { XYState, XYDataLayerConfig, XYReferenceLineLayerConfig } from './types'; +import type { + XYState, + XYDataLayerConfig, + XYReferenceLineLayerConfig, + YAxisMode, + YConfig, +} from './types'; import { checkScaleOperation, getAxisName, @@ -35,7 +40,7 @@ export interface ReferenceLineBase { * * what groups are current defined in data layers * * what existing reference line are currently defined in reference layers */ -export function getGroupsToShow( +export function getGroupsToShow( referenceLayers: T[], state: XYState | undefined, datasourceLayers: DatasourceLayers, diff --git a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts index f37ad12f606b1..975676e241b76 100644 --- a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts +++ b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts @@ -6,13 +6,14 @@ */ import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; -import type { SeriesType, ExtendedYConfig } from '@kbn/expression-xy-plugin/common'; import type { FramePublicAPI, DatasourcePublicAPI } from '../types'; import { visualizationTypes, XYLayerConfig, XYDataLayerConfig, XYReferenceLineLayerConfig, + SeriesType, + YConfig, } from './types'; import { getDataLayers, isAnnotationsLayer, isDataLayer } from './visualization_helpers'; @@ -58,8 +59,7 @@ export const getSeriesColor = (layer: XYLayerConfig, accessor: string) => { return null; } return ( - layer?.yConfig?.find((yConfig: ExtendedYConfig) => yConfig.forAccessor === accessor)?.color || - null + layer?.yConfig?.find((yConfig: YConfig) => yConfig.forAccessor === accessor)?.color || null ); }; diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts index d7440f6f7a386..f0a2b9f28623d 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -159,13 +159,23 @@ describe('#toExpression', () => { undefined, datasourceExpressionsByLayers ) as Ast; - expect( - (expression.chain[0].arguments.axisTitlesVisibilitySettings[0] as Ast).chain[0].arguments - ).toEqual({ - x: [true], - yLeft: [true], - yRight: [true], - }); + expect((expression.chain[0].arguments.yAxisConfigs[0] as Ast).chain[0].arguments).toEqual( + expect.objectContaining({ + showTitle: [true], + position: ['left'], + }) + ); + expect((expression.chain[0].arguments.yAxisConfigs[1] as Ast).chain[0].arguments).toEqual( + expect.objectContaining({ + showTitle: [true], + position: ['right'], + }) + ); + expect((expression.chain[0].arguments.xAxisConfig[0] as Ast).chain[0].arguments).toEqual( + expect.objectContaining({ + showTitle: [true], + }) + ); }); it('should generate an expression without x accessor', () => { @@ -244,9 +254,6 @@ describe('#toExpression', () => { expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b'); expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('c'); expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('d'); - expect(expression.chain[0].arguments.xTitle).toEqual(['']); - expect(expression.chain[0].arguments.yTitle).toEqual(['']); - expect(expression.chain[0].arguments.yRightTitle).toEqual(['']); expect( (expression.chain[0].arguments.layers[0] as Ast).chain[0].arguments.columnToLabel ).toEqual([ @@ -279,13 +286,23 @@ describe('#toExpression', () => { undefined, datasourceExpressionsByLayers ) as Ast; - expect( - (expression.chain[0].arguments.tickLabelsVisibilitySettings[0] as Ast).chain[0].arguments - ).toEqual({ - x: [true], - yLeft: [true], - yRight: [true], - }); + expect((expression.chain[0].arguments.yAxisConfigs[0] as Ast).chain[0].arguments).toEqual( + expect.objectContaining({ + showLabels: [true], + position: ['left'], + }) + ); + expect((expression.chain[0].arguments.yAxisConfigs[1] as Ast).chain[0].arguments).toEqual( + expect.objectContaining({ + showLabels: [true], + position: ['right'], + }) + ); + expect((expression.chain[0].arguments.xAxisConfig[0] as Ast).chain[0].arguments).toEqual( + expect.objectContaining({ + showLabels: [true], + }) + ); }); it('should default the tick labels orientation settings to 0', () => { @@ -309,11 +326,23 @@ describe('#toExpression', () => { undefined, datasourceExpressionsByLayers ) as Ast; - expect((expression.chain[0].arguments.labelsOrientation[0] as Ast).chain[0].arguments).toEqual({ - x: [0], - yLeft: [0], - yRight: [0], - }); + expect((expression.chain[0].arguments.yAxisConfigs[0] as Ast).chain[0].arguments).toEqual( + expect.objectContaining({ + labelsOrientation: [0], + position: ['left'], + }) + ); + expect((expression.chain[0].arguments.yAxisConfigs[1] as Ast).chain[0].arguments).toEqual( + expect.objectContaining({ + labelsOrientation: [0], + position: ['right'], + }) + ); + expect((expression.chain[0].arguments.xAxisConfig[0] as Ast).chain[0].arguments).toEqual( + expect.objectContaining({ + labelsOrientation: [0], + }) + ); }); it('should default the gridlines visibility settings to true', () => { @@ -337,13 +366,23 @@ describe('#toExpression', () => { undefined, datasourceExpressionsByLayers ) as Ast; - expect( - (expression.chain[0].arguments.gridlinesVisibilitySettings[0] as Ast).chain[0].arguments - ).toEqual({ - x: [true], - yLeft: [true], - yRight: [true], - }); + expect((expression.chain[0].arguments.yAxisConfigs[0] as Ast).chain[0].arguments).toEqual( + expect.objectContaining({ + showGridLines: [true], + position: ['left'], + }) + ); + expect((expression.chain[0].arguments.yAxisConfigs[1] as Ast).chain[0].arguments).toEqual( + expect.objectContaining({ + showGridLines: [true], + position: ['right'], + }) + ); + expect((expression.chain[0].arguments.xAxisConfig[0] as Ast).chain[0].arguments).toEqual( + expect.objectContaining({ + showGridLines: [true], + }) + ); }); it('should correctly report the valueLabels visibility settings', () => { @@ -488,8 +527,9 @@ describe('#toExpression', () => { ) as Ast; function getYConfigColorForLayer(ast: Ast, index: number) { - return ((ast.chain[0].arguments.layers[index] as Ast).chain[0].arguments.yConfig[0] as Ast) - .chain[0].arguments.color; + return ( + (ast.chain[0].arguments.layers[index] as Ast).chain[0].arguments.decorations[0] as Ast + ).chain[0].arguments.color; } expect(getYConfigColorForLayer(expression, 0)).toEqual([]); expect(getYConfigColorForLayer(expression, 1)).toEqual([defaultReferenceLineColor]); diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index a6a4766661b17..1b9ea44fac496 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -8,15 +8,15 @@ import { Ast, AstFunction } from '@kbn/interpreter'; import { Position, ScaleType } from '@elastic/charts'; import type { PaletteRegistry } from '@kbn/coloring'; - import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public'; -import type { YConfig, ExtendedYConfig } from '@kbn/expression-xy-plugin/common'; import { LegendSize } from '@kbn/visualizations-plugin/public'; import { State, + YConfig, XYDataLayerConfig, XYReferenceLineLayerConfig, XYAnnotationLayerConfig, + AxisConfig, } from './types'; import type { ValidXYDataLayerConfig } from './types'; import { OperationMetadata, DatasourcePublicAPI, DatasourceLayers } from '../types'; @@ -191,6 +191,54 @@ export const buildExpression = ( return null; } + const isLeftAxis = validDataLayers.some(({ yConfig }) => + yConfig?.some((config) => config.axisMode === Position.Left) + ); + const isRightAxis = validDataLayers.some(({ yConfig }) => + yConfig?.some((config) => config.axisMode === Position.Right) + ); + + const yAxisConfigs: AxisConfig[] = [ + { + position: Position.Left, + extent: state?.yLeftExtent, + showTitle: state?.axisTitlesVisibilitySettings?.yLeft ?? true, + title: state.yTitle || '', + showLabels: state?.tickLabelsVisibilitySettings?.yLeft ?? true, + showGridLines: state?.gridlinesVisibilitySettings?.yLeft ?? true, + labelsOrientation: state?.labelsOrientation?.yLeft ?? 0, + scaleType: state.yLeftScale || 'linear', + }, + { + position: Position.Right, + extent: state?.yRightExtent, + showTitle: state?.axisTitlesVisibilitySettings?.yRight ?? true, + title: state.yRightTitle || '', + showLabels: state?.tickLabelsVisibilitySettings?.yRight ?? true, + showGridLines: state?.gridlinesVisibilitySettings?.yRight ?? true, + labelsOrientation: state?.labelsOrientation?.yRight ?? 0, + scaleType: state.yRightScale || 'linear', + }, + ]; + + if (isLeftAxis) { + yAxisConfigs.push({ + id: Position.Left, + position: Position.Left, + // we need also settings from global config here so that default's doesn't override it + ...yAxisConfigs[0], + }); + } + + if (isRightAxis) { + yAxisConfigs.push({ + id: Position.Right, + position: Position.Right, + // we need also settings from global config here so that default's doesn't override it + ...yAxisConfigs[1], + }); + } + return { type: 'expression', chain: [ @@ -198,9 +246,6 @@ export const buildExpression = ( type: 'function', function: 'layeredXyVis', arguments: { - xTitle: [state.xTitle || ''], - yTitle: [state.yTitle || ''], - yRightTitle: [state.yRightTitle || ''], legend: [ { type: 'expression', @@ -252,82 +297,36 @@ export const buildExpression = ( emphasizeFitting: [state.emphasizeFitting || false], curveType: [state.curveType || 'LINEAR'], fillOpacity: [state.fillOpacity || 0.3], - xExtent: [axisExtentConfigToExpression(state.xExtent)], - yLeftExtent: [axisExtentConfigToExpression(state.yLeftExtent)], - yRightExtent: [axisExtentConfigToExpression(state.yRightExtent)], - yLeftScale: [state.yLeftScale || 'linear'], - yRightScale: [state.yRightScale || 'linear'], - axisTitlesVisibilitySettings: [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'axisTitlesVisibilityConfig', - arguments: { - x: [state?.axisTitlesVisibilitySettings?.x ?? true], - yLeft: [state?.axisTitlesVisibilitySettings?.yLeft ?? true], - yRight: [state?.axisTitlesVisibilitySettings?.yRight ?? true], - }, - }, - ], - }, - ], - tickLabelsVisibilitySettings: [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'tickLabelsConfig', - arguments: { - x: [state?.tickLabelsVisibilitySettings?.x ?? true], - yLeft: [state?.tickLabelsVisibilitySettings?.yLeft ?? true], - yRight: [state?.tickLabelsVisibilitySettings?.yRight ?? true], - }, - }, - ], - }, - ], - gridlinesVisibilitySettings: [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'gridlinesConfig', - arguments: { - x: [state?.gridlinesVisibilitySettings?.x ?? true], - yLeft: [state?.gridlinesVisibilitySettings?.yLeft ?? true], - yRight: [state?.gridlinesVisibilitySettings?.yRight ?? true], - }, - }, - ], - }, - ], - labelsOrientation: [ + valueLabels: [state?.valueLabels || 'hide'], + hideEndzones: [state?.hideEndzones || false], + valuesInLegend: [state?.valuesInLegend || false], + yAxisConfigs: [...yAxisConfigsToExpression(yAxisConfigs)], + xAxisConfig: [ { type: 'expression', chain: [ { type: 'function', - function: 'labelsOrientationConfig', + function: 'xAxisConfig', arguments: { - x: [state?.labelsOrientation?.x ?? 0], - yLeft: [state?.labelsOrientation?.yLeft ?? 0], - yRight: [state?.labelsOrientation?.yRight ?? 0], + id: ['x'], + position: ['bottom'], + title: [state.xTitle || ''], + showTitle: [state?.axisTitlesVisibilitySettings?.x ?? true], + showLabels: [state?.tickLabelsVisibilitySettings?.x ?? true], + showGridLines: [state?.gridlinesVisibilitySettings?.x ?? true], + labelsOrientation: [state?.labelsOrientation?.x ?? 0], + extent: state.xExtent ? [axisExtentConfigToExpression(state.xExtent)] : [], }, }, ], }, ], - valueLabels: [state?.valueLabels || 'hide'], - hideEndzones: [state?.hideEndzones || false], - valuesInLegend: [state?.valuesInLegend || false], layers: [ ...validDataLayers.map((layer) => dataLayerToExpression( layer, + yAxisConfigs, datasourceLayers[layer.layerId], metadata, paletteService, @@ -351,6 +350,29 @@ export const buildExpression = ( }; }; +const yAxisConfigsToExpression = (yAxisConfigs: AxisConfig[]): Ast[] => { + return yAxisConfigs.map((axis) => ({ + type: 'expression', + chain: [ + { + type: 'function', + function: 'yAxisConfig', + arguments: { + id: axis.id ? [axis.id] : [], + position: axis.position ? [axis.position] : [], + extent: axis.extent ? [axisExtentConfigToExpression(axis.extent)] : [], + showTitle: [axis.showTitle ?? true], + title: axis.title !== undefined ? [axis.title] : [], + showLabels: [axis.showLabels ?? true], + showGridLines: [axis.showGridLines ?? true], + labelsOrientation: axis.labelsOrientation !== undefined ? [axis.labelsOrientation] : [], + scaleType: axis.scaleType ? [axis.scaleType] : [], + }, + }, + ], + })); +}; + const referenceLineLayerToExpression = ( layer: XYReferenceLineLayerConfig, datasourceLayer: DatasourcePublicAPI, @@ -364,9 +386,9 @@ const referenceLineLayerToExpression = ( function: 'referenceLineLayer', arguments: { layerId: [layer.layerId], - yConfig: layer.yConfig + decorations: layer.yConfig ? layer.yConfig.map((yConfig) => - extendedYConfigToExpression(yConfig, defaultReferenceLineColor) + extendedYConfigToRLDecorationConfigExpression(yConfig, defaultReferenceLineColor) ) : [], accessors: layer.accessors, @@ -402,6 +424,7 @@ const annotationLayerToExpression = ( const dataLayerToExpression = ( layer: ValidXYDataLayerConfig, + yAxisConfigs: AxisConfig[], datasourceLayer: DatasourcePublicAPI, metadata: Record>, paletteService: PaletteRegistry, @@ -418,6 +441,12 @@ const dataLayerToExpression = ( xAxisOperation.scale !== 'ordinal' ); + const dataFromType = layer.seriesType.split('_'); + const seriesType = dataFromType[0]; + const isPercentage = dataFromType.includes('percentage'); + const isStacked = dataFromType.includes('stacked'); + const isHorizontal = dataFromType.includes('horizontal'); + return { type: 'expression', chain: [ @@ -430,11 +459,16 @@ const dataLayerToExpression = ( xAccessor: layer.xAccessor ? [layer.xAccessor] : [], xScaleType: [getScaleType(metadata[layer.layerId][layer.xAccessor], ScaleType.Linear)], isHistogram: [isHistogramDimension], + isPercentage: isPercentage ? [isPercentage] : [], + isStacked: isStacked ? [isStacked] : [], + isHorizontal: isHorizontal ? [isHorizontal] : [], splitAccessor: layer.collapseFn || !layer.splitAccessor ? [] : [layer.splitAccessor], - yConfig: layer.yConfig - ? layer.yConfig.map((yConfig) => yConfigToExpression(yConfig)) + decorations: layer.yConfig + ? layer.yConfig.map((yConfig) => + yConfigToDataDecorationConfigExpression(yConfig, yAxisConfigs) + ) : [], - seriesType: [layer.seriesType], + seriesType: [seriesType], accessors: layer.accessors, columnToLabel: [JSON.stringify(columnToLabel)], ...(datasourceExpression @@ -493,16 +527,21 @@ const dataLayerToExpression = ( }; }; -const yConfigToExpression = (yConfig: YConfig, defaultColor?: string): Ast => { +const yConfigToDataDecorationConfigExpression = ( + yConfig: YConfig, + yAxisConfigs: AxisConfig[], + defaultColor?: string +): Ast => { + const axisId = yAxisConfigs.find((axis) => axis.id && axis.position === yConfig.axisMode)?.id; return { type: 'expression', chain: [ { type: 'function', - function: 'yConfig', + function: 'dataDecorationConfig', arguments: { + axisId: axisId ? [axisId] : [], forAccessor: [yConfig.forAccessor], - axisMode: yConfig.axisMode ? [yConfig.axisMode] : [], color: yConfig.color ? [yConfig.color] : defaultColor ? [defaultColor] : [], }, }, @@ -510,16 +549,19 @@ const yConfigToExpression = (yConfig: YConfig, defaultColor?: string): Ast => { }; }; -const extendedYConfigToExpression = (yConfig: ExtendedYConfig, defaultColor?: string): Ast => { +const extendedYConfigToRLDecorationConfigExpression = ( + yConfig: YConfig, + defaultColor?: string +): Ast => { return { type: 'expression', chain: [ { type: 'function', - function: 'extendedYConfig', + function: 'referenceLineDecorationConfig', arguments: { forAccessor: [yConfig.forAccessor], - axisMode: yConfig.axisMode ? [yConfig.axisMode] : [], + position: yConfig.axisMode ? [yConfig.axisMode] : [], color: yConfig.color ? [yConfig.color] : defaultColor ? [defaultColor] : [], lineStyle: [yConfig.lineStyle || 'solid'], lineWidth: [yConfig.lineWidth || 1], diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index 67cf7989fce76..0beda9f4740ac 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -6,20 +6,20 @@ */ import { i18n } from '@kbn/i18n'; +import { $Values } from '@kbn/utility-types'; import type { PaletteOutput } from '@kbn/coloring'; import type { - SeriesType, LegendConfig, AxisExtentConfig, XYCurveType, - AxesSettingsConfig, FittingFunction, - LabelsOrientationConfig, EndValue, - ExtendedYConfig, - YConfig, YScaleType, XScaleType, + LineStyle, + IconPosition, + FillStyle, + YAxisConfig, } from '@kbn/expression-xy-plugin/common'; import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common'; import { LensIconChartArea } from '../assets/chart_area'; @@ -36,6 +36,56 @@ import { LensIconChartLine } from '../assets/chart_line'; import type { VisualizationType, Suggestion } from '../types'; import type { ValueLabelConfig } from '../../common/types'; +export const YAxisModes = { + AUTO: 'auto', + LEFT: 'left', + RIGHT: 'right', + BOTTOM: 'bottom', +} as const; + +export const SeriesTypes = { + BAR: 'bar', + LINE: 'line', + AREA: 'area', + BAR_STACKED: 'bar_stacked', + AREA_STACKED: 'area_stacked', + BAR_HORIZONTAL: 'bar_horizontal', + BAR_PERCENTAGE_STACKED: 'bar_percentage_stacked', + BAR_HORIZONTAL_STACKED: 'bar_horizontal_stacked', + AREA_PERCENTAGE_STACKED: 'area_percentage_stacked', + BAR_HORIZONTAL_PERCENTAGE_STACKED: 'bar_horizontal_percentage_stacked', +} as const; + +export type YAxisMode = $Values; +export type SeriesType = $Values; +export interface AxesSettingsConfig { + x: boolean; + yRight: boolean; + yLeft: boolean; +} + +export interface AxisConfig extends Omit { + extent?: AxisExtentConfig; +} + +export interface LabelsOrientationConfig { + x: number; + yLeft: number; + yRight: number; +} + +export interface YConfig { + forAccessor: string; + color?: string; + icon?: string; + lineWidth?: number; + lineStyle?: LineStyle; + fill?: FillStyle; + iconPosition?: IconPosition; + textVisibility?: boolean; + axisMode?: YAxisMode; +} + export interface XYDataLayerConfig { layerId: string; accessors: string[]; @@ -55,7 +105,7 @@ export interface XYDataLayerConfig { export interface XYReferenceLineLayerConfig { layerId: string; accessors: string[]; - yConfig?: ExtendedYConfig[]; + yConfig?: YConfig[]; layerType: 'referenceLine'; } diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index 0092bb78a6d71..1a4b944a9764c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -15,9 +15,9 @@ import type { XYLayerConfig, XYDataLayerConfig, XYReferenceLineLayerConfig, + SeriesType, } from './types'; import { createDatatableUtilitiesMock } from '@kbn/data-plugin/common/mocks'; -import type { SeriesType } from '@kbn/expression-xy-plugin/common'; import { layerTypes } from '../../common'; import { createMockDatasource, createMockFramePublicAPI } from '../mocks'; import { LensIconChartBar } from '../assets/chart_bar'; diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index dcf3ab3e42a47..2eb15c96afe95 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -17,18 +17,22 @@ import { ThemeServiceStart } from '@kbn/core/public'; import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public'; import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public'; -import { - FillStyle, - SeriesType, - YAxisMode, - ExtendedYConfig, -} from '@kbn/expression-xy-plugin/common'; +import { FillStyle } from '@kbn/expression-xy-plugin/common'; import { getSuggestions } from './xy_suggestions'; import { XyToolbar } from './xy_config_panel'; import { DimensionEditor } from './xy_config_panel/dimension_editor'; import { LayerHeader } from './xy_config_panel/layer_header'; -import { Visualization, AccessorConfig, FramePublicAPI } from '../types'; -import { State, visualizationTypes, XYSuggestion, XYLayerConfig, XYDataLayerConfig } from './types'; +import type { Visualization, AccessorConfig, FramePublicAPI } from '../types'; +import { + State, + visualizationTypes, + XYSuggestion, + XYLayerConfig, + XYDataLayerConfig, + YConfig, + YAxisMode, + SeriesType, +} from './types'; import { layerTypes } from '../../common'; import { isHorizontalChart } from './state_helpers'; import { toExpression, toPreviewExpression, getSortedAccessors } from './to_expression'; @@ -360,7 +364,7 @@ export const getXyVisualization = ({ } const isReferenceLine = metrics.some((metric) => metric.agg === 'static_value'); const axisMode = axisPosition as YAxisMode; - const yConfig = metrics.map((metric, idx) => { + const yConfig = metrics.map((metric, idx) => { return { color: metric.color, forAccessor: metric.accessor ?? foundLayer.accessors[idx], diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx index 9b4ed872f1dc2..e89edab464bfd 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization_helpers.tsx @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { uniq } from 'lodash'; -import { SeriesType } from '@kbn/expression-xy-plugin/common'; import { DatasourceLayers, OperationMetadata, VisualizationType } from '../types'; import { State, @@ -17,6 +16,7 @@ import { XYLayerConfig, XYDataLayerConfig, XYReferenceLineLayerConfig, + SeriesType, } from './types'; import { isHorizontalChart } from './state_helpers'; import { layerTypes } from '..'; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx index 21732f0ab1ce0..263a9131e1296 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/axis_settings_popover.tsx @@ -16,9 +16,9 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isEqual } from 'lodash'; -import { AxesSettingsConfig, AxisExtentConfig, YScaleType } from '@kbn/expression-xy-plugin/common'; +import { AxisExtentConfig, YScaleType } from '@kbn/expression-xy-plugin/common'; import { ToolbarButtonProps } from '@kbn/kibana-react-plugin/public'; -import { XYLayerConfig } from '../types'; +import { XYLayerConfig, AxesSettingsConfig } from '../types'; import { ToolbarPopover, useDebouncedValue, diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/dimension_editor.tsx index 60329d613b797..359a3cb0de153 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/dimension_editor.tsx @@ -10,9 +10,8 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonGroup, EuiFormRow, htmlIdGenerator } from '@elastic/eui'; import type { PaletteRegistry } from '@kbn/coloring'; import type { DatatableUtilitiesService } from '@kbn/data-plugin/common'; -import { YAxisMode, ExtendedYConfig } from '@kbn/expression-xy-plugin/common'; import type { VisualizationDimensionEditorProps } from '../../types'; -import { State, XYState, XYDataLayerConfig } from '../types'; +import { State, XYState, XYDataLayerConfig, YConfig, YAxisMode } from '../types'; import { FormatFactory } from '../../../common'; import { getSeriesColor, isHorizontalChart } from '../state_helpers'; import { ColorPicker } from './color_picker'; @@ -81,7 +80,7 @@ export function DataDimensionEditor( const axisMode = localYConfig?.axisMode || 'auto'; const setConfig = useCallback( - (yConfig: Partial | undefined) => { + (yConfig: Partial | undefined) => { if (yConfig == null) { return; } diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx index 9d4ad63a7052a..4a160cf34240c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx @@ -9,10 +9,10 @@ import React, { memo, useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { Position, ScaleType } from '@elastic/charts'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { AxesSettingsConfig, AxisExtentConfig } from '@kbn/expression-xy-plugin/common'; +import { AxisExtentConfig } from '@kbn/expression-xy-plugin/common'; import { LegendSize } from '@kbn/visualizations-plugin/public'; import type { VisualizationToolbarProps, FramePublicAPI } from '../../types'; -import { State, XYState } from '../types'; +import { State, XYState, AxesSettingsConfig } from '../types'; import { isHorizontalChart } from '../state_helpers'; import { hasNumericHistogramDimension, LegendSettingsPopover } from '../../shared_components'; import { AxisSettingsPopover } from './axis_settings_popover'; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx index 3ba95431f85d5..d3a7552954d13 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx @@ -8,10 +8,9 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiIcon, EuiPopover, EuiSelectable, EuiText, EuiPopoverTitle } from '@elastic/eui'; -import { SeriesType } from '@kbn/expression-xy-plugin/common'; import { ToolbarButton } from '@kbn/kibana-react-plugin/public'; import type { VisualizationLayerWidgetProps, VisualizationType } from '../../types'; -import { State, visualizationTypes } from '../types'; +import { State, visualizationTypes, SeriesType } from '../types'; import { isHorizontalChart, isHorizontalSeries } from '../state_helpers'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { StaticHeader } from '../../shared_components'; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_config_panel/reference_line_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_config_panel/reference_line_panel.tsx index 8f9088f61b527..fd4cad44c3244 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_config_panel/reference_line_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_config_panel/reference_line_panel.tsx @@ -9,9 +9,9 @@ import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonGroup, EuiFormRow } from '@elastic/eui'; import type { PaletteRegistry } from '@kbn/coloring'; -import { FillStyle, ExtendedYConfig } from '@kbn/expression-xy-plugin/common'; +import { FillStyle } from '@kbn/expression-xy-plugin/common'; import type { VisualizationDimensionEditorProps } from '../../../types'; -import { State, XYState, XYReferenceLineLayerConfig } from '../../types'; +import { State, XYState, XYReferenceLineLayerConfig, YConfig } from '../../types'; import { FormatFactory } from '../../../../common'; import { ColorPicker } from '../color_picker'; @@ -52,7 +52,7 @@ export const ReferenceLinePanel = ( ); const setConfig = useCallback( - (yConfig: Partial | undefined) => { + (yConfig: Partial | undefined) => { if (yConfig == null) { return; } @@ -108,7 +108,7 @@ export const ReferenceLinePanel = ( interface LabelConfigurationOptions { isHorizontal: boolean; - axisMode: ExtendedYConfig['axisMode']; + axisMode: YConfig['axisMode']; } function getFillPositionOptions({ isHorizontal, axisMode }: LabelConfigurationOptions) { @@ -154,8 +154,8 @@ export const FillSetting = ({ setConfig, isHorizontal, }: { - currentConfig?: ExtendedYConfig; - setConfig: (yConfig: Partial | undefined) => void; + currentConfig?: YConfig; + setConfig: (yConfig: Partial | undefined) => void; isHorizontal: boolean; }) => { return ( diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/marker_decoration_settings.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/marker_decoration_settings.tsx index 64b00ef246161..561e336400a37 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/marker_decoration_settings.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/marker_decoration_settings.tsx @@ -8,7 +8,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonGroup, EuiFormRow } from '@elastic/eui'; -import { IconPosition, YAxisMode } from '@kbn/expression-xy-plugin/common'; +import { IconPosition } from '@kbn/expression-xy-plugin/common'; +import { YAxisMode } from '../../types'; import { TooltipWrapper } from '../../../shared_components'; import { hasIcon, IconSelect, IconSet } from './icon_select'; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index 51e1c5fe2a954..ca42aabe616b6 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n'; import { partition } from 'lodash'; import { Position } from '@elastic/charts'; import type { PaletteOutput } from '@kbn/coloring'; -import type { SeriesType } from '@kbn/expression-xy-plugin/common'; import { SuggestionRequest, VisualizationSuggestion, @@ -17,7 +16,14 @@ import { TableSuggestion, TableChangeType, } from '../types'; -import { State, XYState, visualizationTypes, XYLayerConfig, XYDataLayerConfig } from './types'; +import { + State, + XYState, + visualizationTypes, + XYLayerConfig, + XYDataLayerConfig, + SeriesType, +} from './types'; import { layerTypes } from '../../common'; import { getIconForSeries } from './state_helpers'; import { getDataLayers, isDataLayer } from './visualization_helpers'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 65daef7ac85f6..740c45993ef04 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -29,6 +29,7 @@ import { TypedLensByValueInput, XYCurveType, XYState, + YAxisMode, } from '@kbn/lens-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; import { PersistableFilter } from '@kbn/lens-plugin/common'; @@ -900,8 +901,8 @@ export class LensAttributes { this.layerConfigs[0].indexPattern.fieldFormatMap[ this.layerConfigs[0].selectedMetricField ]?.id - ? 'left' - : 'right', + ? ('left' as YAxisMode) + : ('right' as YAxisMode), }, ], xAccessor: `x-axis-column-layer${index}`, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 74d0578d459f9..85fbf2e073ecb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -13,7 +13,7 @@ import type { FieldBasedIndexPatternColumn, SeriesType, OperationType, - ExtendedYConfig, + YConfig, } from '@kbn/lens-plugin/public'; import type { PersistableFilter } from '@kbn/lens-plugin/common'; @@ -84,7 +84,7 @@ export interface SeriesConfig { hasOperationType: boolean; palette?: PaletteOutput; yTitle?: string; - yConfig?: ExtendedYConfig[]; + yConfig?: YConfig[]; query?: { query: string; language: 'kuery' }; } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 59c45ebb3e98b..f410b448c6075 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -3402,10 +3402,6 @@ "expressionXY.axisExtentConfig.help": "Configurer les étendues d’axe du graphique xy", "expressionXY.axisExtentConfig.lowerBound.help": "Limite inférieure", "expressionXY.axisExtentConfig.upperBound.help": "Limite supérieure", - "expressionXY.axisTitlesVisibilityConfig.help": "Configurer l’aspect des titres d’axe du graphique xy", - "expressionXY.axisTitlesVisibilityConfig.x.help": "Spécifie si le titre de l'axe X est visible ou non.", - "expressionXY.axisTitlesVisibilityConfig.yLeft.help": "Spécifie si le titre de l'axe Y de gauche est visible ou non.", - "expressionXY.axisTitlesVisibilityConfig.yRight.help": "Spécifie si le titre de l'axe Y de droite est visible ou non.", "expressionXY.dataLayer.accessors.help": "Les colonnes à afficher sur l’axe y.", "expressionXY.layer.columnToLabel.help": "Paires clé-valeur JSON de l’ID de colonne pour l’étiquette", "expressionXY.dataLayer.help": "Configurer un calque dans le graphique xy", @@ -3417,15 +3413,6 @@ "expressionXY.dataLayer.splitAccessor.help": "Colonne selon laquelle effectuer la division", "expressionXY.dataLayer.xAccessor.help": "Axe X", "expressionXY.dataLayer.xScaleType.help": "Type d’échelle de l’axe x", - "expressionXY.dataLayer.yConfig.help": "Configuration supplémentaire pour les axes y", - "expressionXY.gridlinesConfig.help": "Configurer l’aspect du quadrillage du graphique xy", - "expressionXY.gridlinesConfig.x.help": "Spécifie si le quadrillage de l'axe X est visible ou non.", - "expressionXY.gridlinesConfig.yLeft.help": "Spécifie si le quadrillage de l'axe Y de gauche est visible ou non.", - "expressionXY.gridlinesConfig.yRight.help": "Spécifie si le quadrillage de l'axe Y de droite est visible ou non.", - "expressionXY.labelsOrientationConfig.help": "Configurer l’orientation des étiquettes de coche du graphique xy", - "expressionXY.labelsOrientationConfig.x.help": "Spécifie l'orientation des étiquettes de l'axe X.", - "expressionXY.labelsOrientationConfig.yLeft.help": "Spécifie l'orientation des étiquettes de l'axe Y de gauche.", - "expressionXY.labelsOrientationConfig.yRight.help": "Spécifie l'orientation des étiquettes de l'axe Y de droite.", "expressionXY.legend.filterForValueButtonAriaLabel": "Filtrer sur la valeur", "expressionXY.legend.filterOptionsLegend": "{legendDataLabel}, options de filtre", "expressionXY.legend.filterOutValueButtonAriaLabel": "Exclure la valeur", @@ -3442,11 +3429,6 @@ "expressionXY.legendConfig.verticalAlignment.help": "Spécifie l'alignement vertical de la légende lorsqu'elle est affichée à l'intérieur du graphique.", "expressionXY.referenceLineLayer.accessors.help": "Les colonnes à afficher sur l’axe y.", "expressionXY.referenceLineLayer.help": "Configurer une ligne de référence dans le graphique xy", - "expressionXY.referenceLineLayer.yConfig.help": "Configuration supplémentaire pour les axes y", - "expressionXY.tickLabelsConfig.help": "Configurer l’aspect des étiquettes de coche du graphique xy", - "expressionXY.tickLabelsConfig.x.help": "Spécifie si les étiquettes de graduation de l'axe X sont visibles ou non.", - "expressionXY.tickLabelsConfig.yLeft.help": "Spécifie si les étiquettes de graduation de l'axe Y de gauche sont visibles ou non.", - "expressionXY.tickLabelsConfig.yRight.help": "Spécifie si les étiquettes de graduation de l'axe Y de droite sont visibles ou non.", "expressionXY.xyChart.emptyXLabel": "(vide)", "expressionXY.xyChart.iconSelect.alertIconLabel": "Alerte", "expressionXY.xyChart.iconSelect.asteriskIconLabel": "Astérisque", @@ -3463,39 +3445,20 @@ "expressionXY.xyChart.iconSelect.tagIconLabel": "Balise", "expressionXY.xyChart.iconSelect.triangleIconLabel": "Triangle", "expressionXY.xyVis.ariaLabel.help": "Spécifie l’attribut aria-label du graphique xy", - "expressionXY.xyVis.axisTitlesVisibilitySettings.help": "Afficher les titres des axes X et Y", "expressionXY.xyVis.curveType.help": "Définir de quelle façon le type de courbe est rendu pour un graphique linéaire", "expressionXY.xyVis.endValue.help": "Valeur de fin", "expressionXY.xyVis.fillOpacity.help": "Définir l'opacité du remplissage du graphique en aires", "expressionXY.xyVis.fittingFunction.help": "Définir le mode de traitement des valeurs manquantes", - "expressionXY.xyVis.gridlinesVisibilitySettings.help": "Afficher le quadrillage des axes X et Y", "expressionXY.xyVis.help": "Graphique X/Y", "expressionXY.xyVis.hideEndzones.help": "Masquer les marqueurs de zone de fin pour les données partielles", - "expressionXY.xyVis.labelsOrientation.help": "Définit la rotation des étiquettes des axes", "expressionXY.layeredXyVis.layers.help": "Calques de série visuelle", "expressionXY.xyVis.legend.help": "Configurez la légende du graphique.", "expressionXY.xyVis.logDatatable.breakDown": "Répartir par", "expressionXY.xyVis.logDatatable.metric": "Axe vertical", "expressionXY.xyVis.logDatatable.x": "Axe horizontal", "expressionXY.xyVis.renderer.help": "Outil de rendu de graphique X/Y", - "expressionXY.xyVis.tickLabelsVisibilitySettings.help": "Afficher les étiquettes de graduation des axes X et Y", "expressionXY.xyVis.valueLabels.help": "Mode des étiquettes de valeur", "expressionXY.xyVis.valuesInLegend.help": "Afficher les valeurs dans la légende", - "expressionXY.xyVis.xTitle.help": "Titre de l'axe X", - "expressionXY.xyVis.yLeftExtent.help": "Portée de l'axe Y de gauche", - "expressionXY.xyVis.yLeftTitle.help": "Titre de l'axe Y de gauche", - "expressionXY.xyVis.yRightExtent.help": "Portée de l'axe Y de droite", - "expressionXY.xyVis.yRightTitle.help": "Titre de l'axe Y de droite", - "expressionXY.yConfig.axisMode.help": "Le mode axe de l’indicateur", - "expressionXY.yConfig.color.help": "La couleur des séries", - "expressionXY.yConfig.fill.help": "Remplir", - "expressionXY.yConfig.forAccessor.help": "L’accesseur auquel cette configuration s’applique", - "expressionXY.yConfig.help": "Configurer le comportement de l’indicateur d’axe y d’un graphique xy", - "expressionXY.yConfig.icon.help": "Icône facultative utilisée pour les lignes de référence", - "expressionXY.yConfig.iconPosition.help": "Le placement de l’icône pour la ligne de référence", - "expressionXY.yConfig.lineStyle.help": "Le style de la ligne de référence", - "expressionXY.yConfig.lineWidth.help": "La largeur de la ligne de référence", - "expressionXY.yConfig.textVisibility.help": "Visibilité de l’étiquette sur la ligne de référence", "fieldFormats.advancedSettings.format.bytesFormat.numeralFormatLinkText": "Format numérique", "fieldFormats.advancedSettings.format.bytesFormatText": "{numeralFormatLink} par défaut pour le format \"octets\"", "fieldFormats.advancedSettings.format.bytesFormatTitle": "Format octets", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index dfb400097a5f9..c4bc0ad2f8620 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3501,10 +3501,6 @@ "expressionXY.axisExtentConfig.help": "xyグラフの軸範囲を構成", "expressionXY.axisExtentConfig.lowerBound.help": "下界", "expressionXY.axisExtentConfig.upperBound.help": "上界", - "expressionXY.axisTitlesVisibilityConfig.help": "xyグラフの軸タイトル表示を構成", - "expressionXY.axisTitlesVisibilityConfig.x.help": "x軸のタイトルを表示するかどうかを指定します。", - "expressionXY.axisTitlesVisibilityConfig.yLeft.help": "左y軸のタイトルを表示するかどうかを指定します。", - "expressionXY.axisTitlesVisibilityConfig.yRight.help": "右y軸のタイトルを表示するかどうかを指定します。", "expressionXY.dataLayer.accessors.help": "y軸に表示する列。", "expressionXY.layer.columnToLabel.help": "ラベリングする列IDのJSONキー値のペア", "expressionXY.dataLayer.help": "xyグラフでレイヤーを構成", @@ -3516,15 +3512,6 @@ "expressionXY.dataLayer.splitAccessor.help": "分割の基準となる列", "expressionXY.dataLayer.xAccessor.help": "X 軸", "expressionXY.dataLayer.xScaleType.help": "x軸の目盛タイプ", - "expressionXY.dataLayer.yConfig.help": "y軸の詳細構成", - "expressionXY.gridlinesConfig.help": "xyグラフのグリッド線表示を構成", - "expressionXY.gridlinesConfig.x.help": "x 軸のグリッド線を表示するかどうかを指定します。", - "expressionXY.gridlinesConfig.yLeft.help": "左y軸のグリッド線を表示するかどうかを指定します。", - "expressionXY.gridlinesConfig.yRight.help": "右y軸のグリッド線を表示するかどうかを指定します。", - "expressionXY.labelsOrientationConfig.help": "xyグラフのティックラベルの向きを構成", - "expressionXY.labelsOrientationConfig.x.help": "x軸のラベルの向きを指定します。", - "expressionXY.labelsOrientationConfig.yLeft.help": "左y軸のラベルの向きを指定します。", - "expressionXY.labelsOrientationConfig.yRight.help": "右y軸のラベルの向きを指定します。", "expressionXY.legend.filterForValueButtonAriaLabel": "値でフィルター", "expressionXY.legend.filterOptionsLegend": "{legendDataLabel}、フィルターオプション", "expressionXY.legend.filterOutValueButtonAriaLabel": "値を除外", @@ -3541,11 +3528,6 @@ "expressionXY.legendConfig.verticalAlignment.help": "凡例がグラフ内に表示されるときに凡例の縦の配置を指定します。", "expressionXY.referenceLineLayer.accessors.help": "y軸に表示する列。", "expressionXY.referenceLineLayer.help": "xyグラフで基準線を構成", - "expressionXY.referenceLineLayer.yConfig.help": "y軸の詳細構成", - "expressionXY.tickLabelsConfig.help": "xyグラフのティックラベルの表示を構成", - "expressionXY.tickLabelsConfig.x.help": "x軸の目盛ラベルを表示するかどうかを指定します。", - "expressionXY.tickLabelsConfig.yLeft.help": "左y軸の目盛ラベルを表示するかどうかを指定します。", - "expressionXY.tickLabelsConfig.yRight.help": "右y軸の目盛ラベルを表示するかどうかを指定します。", "expressionXY.xyChart.emptyXLabel": "(空)", "expressionXY.xyChart.iconSelect.alertIconLabel": "アラート", "expressionXY.xyChart.iconSelect.asteriskIconLabel": "アスタリスク", @@ -3562,39 +3544,20 @@ "expressionXY.xyChart.iconSelect.tagIconLabel": "タグ", "expressionXY.xyChart.iconSelect.triangleIconLabel": "三角形", "expressionXY.xyVis.ariaLabel.help": "xyグラフのariaラベルを指定します", - "expressionXY.xyVis.axisTitlesVisibilitySettings.help": "xおよびy軸のタイトルを表示", "expressionXY.xyVis.curveType.help": "折れ線グラフで曲線タイプをレンダリングする方法を定義します", "expressionXY.xyVis.endValue.help": "終了値", "expressionXY.xyVis.fillOpacity.help": "エリアグラフの塗りつぶしの透明度を定義", "expressionXY.xyVis.fittingFunction.help": "欠測値の処理方法を定義", - "expressionXY.xyVis.gridlinesVisibilitySettings.help": "xおよびy軸のグリッド線を表示", "expressionXY.xyVis.help": "X/Y チャート", "expressionXY.xyVis.hideEndzones.help": "部分データの終了ゾーンマーカーを非表示", - "expressionXY.xyVis.labelsOrientation.help": "軸ラベルの回転を定義します", "expressionXY.layeredXyVis.layers.help": "視覚的な系列のレイヤー", "expressionXY.xyVis.legend.help": "チャートの凡例を構成します。", "expressionXY.xyVis.logDatatable.breakDown": "内訳の基準", "expressionXY.xyVis.logDatatable.metric": "縦軸", "expressionXY.xyVis.logDatatable.x": "横軸", "expressionXY.xyVis.renderer.help": "X/Y チャートを再レンダリング", - "expressionXY.xyVis.tickLabelsVisibilitySettings.help": "xおよびy軸の目盛ラベルを表示", "expressionXY.xyVis.valueLabels.help": "値ラベルモード", "expressionXY.xyVis.valuesInLegend.help": "凡例に値を表示", - "expressionXY.xyVis.xTitle.help": "x軸のタイトル", - "expressionXY.xyVis.yLeftExtent.help": "Y左軸範囲", - "expressionXY.xyVis.yLeftTitle.help": "左y軸のタイトル", - "expressionXY.xyVis.yRightExtent.help": "Y右軸範囲", - "expressionXY.xyVis.yRightTitle.help": "右 y 軸のタイトル", - "expressionXY.yConfig.axisMode.help": "メトリックの軸モード", - "expressionXY.yConfig.color.help": "系列の色", - "expressionXY.yConfig.fill.help": "塗りつぶし", - "expressionXY.yConfig.forAccessor.help": "この構成のアクセサー", - "expressionXY.yConfig.help": "xyグラフのy軸メトリックの動作を構成", - "expressionXY.yConfig.icon.help": "基準線で使用される任意のアイコン", - "expressionXY.yConfig.iconPosition.help": "基準線のアイコンの配置", - "expressionXY.yConfig.lineStyle.help": "基準線のスタイル", - "expressionXY.yConfig.lineWidth.help": "基準線の幅", - "expressionXY.yConfig.textVisibility.help": "基準線のラベルの表示", "fieldFormats.advancedSettings.format.bytesFormat.numeralFormatLinkText": "数字フォーマット", "fieldFormats.advancedSettings.format.bytesFormatText": "「バイト」フォーマットのデフォルト{numeralFormatLink}です", "fieldFormats.advancedSettings.format.bytesFormatTitle": "バイトフォーマット", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 1601b254b4d98..3cf3a38b00848 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3506,10 +3506,6 @@ "expressionXY.axisExtentConfig.help": "配置 xy 图表的轴范围", "expressionXY.axisExtentConfig.lowerBound.help": "下边界", "expressionXY.axisExtentConfig.upperBound.help": "上边界", - "expressionXY.axisTitlesVisibilityConfig.help": "配置 xy 图表的轴标题外观", - "expressionXY.axisTitlesVisibilityConfig.x.help": "指定 x 轴的标题是否可见。", - "expressionXY.axisTitlesVisibilityConfig.yLeft.help": "指定左侧 y 轴的标题是否可见。", - "expressionXY.axisTitlesVisibilityConfig.yRight.help": "指定右侧 y 轴的标题是否可见。", "expressionXY.dataLayer.accessors.help": "要在 y 轴上显示的列。", "expressionXY.layer.columnToLabel.help": "要标记的列 ID 的 JSON 键值对", "expressionXY.dataLayer.help": "配置 xy 图表中的图层", @@ -3521,15 +3517,6 @@ "expressionXY.dataLayer.splitAccessor.help": "拆分要依据的列", "expressionXY.dataLayer.xAccessor.help": "X 轴", "expressionXY.dataLayer.xScaleType.help": "x 轴的缩放类型", - "expressionXY.dataLayer.yConfig.help": "y 轴的其他配置", - "expressionXY.gridlinesConfig.help": "配置 xy 图表的网格线外观", - "expressionXY.gridlinesConfig.x.help": "指定 x 轴的网格线是否可见。", - "expressionXY.gridlinesConfig.yLeft.help": "指定左侧 y 轴的网格线是否可见。", - "expressionXY.gridlinesConfig.yRight.help": "指定右侧 y 轴的网格线是否可见。", - "expressionXY.labelsOrientationConfig.help": "配置 xy 图表的刻度标签方向", - "expressionXY.labelsOrientationConfig.x.help": "指定 x 轴的标签方向。", - "expressionXY.labelsOrientationConfig.yLeft.help": "指定左 y 轴的标签方向。", - "expressionXY.labelsOrientationConfig.yRight.help": "指定右 y 轴的标签方向。", "expressionXY.legend.filterForValueButtonAriaLabel": "筛留值", "expressionXY.legend.filterOptionsLegend": "{legendDataLabel}, 筛选选项", "expressionXY.legend.filterOutValueButtonAriaLabel": "筛除值", @@ -3546,11 +3533,6 @@ "expressionXY.legendConfig.verticalAlignment.help": "指定图例显示在图表内时垂直对齐。", "expressionXY.referenceLineLayer.accessors.help": "要在 y 轴上显示的列。", "expressionXY.referenceLineLayer.help": "配置 xy 图表中的参考线", - "expressionXY.referenceLineLayer.yConfig.help": "y 轴的其他配置", - "expressionXY.tickLabelsConfig.help": "配置 xy 图表的刻度标签外观", - "expressionXY.tickLabelsConfig.x.help": "指定 x 轴的刻度标签是否可见。", - "expressionXY.tickLabelsConfig.yLeft.help": "指定左侧 y 轴的刻度标签是否可见。", - "expressionXY.tickLabelsConfig.yRight.help": "指定右侧 y 轴的刻度标签是否可见。", "expressionXY.xyChart.emptyXLabel": "(空)", "expressionXY.xyChart.iconSelect.alertIconLabel": "告警", "expressionXY.xyChart.iconSelect.asteriskIconLabel": "星号", @@ -3567,39 +3549,20 @@ "expressionXY.xyChart.iconSelect.tagIconLabel": "标签", "expressionXY.xyChart.iconSelect.triangleIconLabel": "三角形", "expressionXY.xyVis.ariaLabel.help": "指定 xy 图表的 aria 标签", - "expressionXY.xyVis.axisTitlesVisibilitySettings.help": "显示 x 和 y 轴标题", "expressionXY.xyVis.curveType.help": "定义为折线图渲染曲线类型的方式", "expressionXY.xyVis.endValue.help": "结束值", "expressionXY.xyVis.fillOpacity.help": "定义面积图填充透明度", "expressionXY.xyVis.fittingFunction.help": "定义处理缺失值的方式", - "expressionXY.xyVis.gridlinesVisibilitySettings.help": "显示 x 和 y 轴网格线", "expressionXY.xyVis.help": "X/Y 图表", "expressionXY.xyVis.hideEndzones.help": "隐藏部分数据的末日区域标记", - "expressionXY.xyVis.labelsOrientation.help": "定义轴标签的旋转", "expressionXY.layeredXyVis.layers.help": "可视序列的图层", "expressionXY.xyVis.legend.help": "配置图表图例。", "expressionXY.xyVis.logDatatable.breakDown": "细分方式", "expressionXY.xyVis.logDatatable.metric": "垂直轴", "expressionXY.xyVis.logDatatable.x": "水平轴", "expressionXY.xyVis.renderer.help": "X/Y 图表呈现器", - "expressionXY.xyVis.tickLabelsVisibilitySettings.help": "显示 x 和 y 轴刻度标签", "expressionXY.xyVis.valueLabels.help": "值标签模式", "expressionXY.xyVis.valuesInLegend.help": "在图例中显示值", - "expressionXY.xyVis.xTitle.help": "X 轴标题", - "expressionXY.xyVis.yLeftExtent.help": "左侧 Y 轴范围", - "expressionXY.xyVis.yLeftTitle.help": "左侧 Y 轴标题", - "expressionXY.xyVis.yRightExtent.help": "右侧 Y 轴范围", - "expressionXY.xyVis.yRightTitle.help": "右侧 Y 轴标题", - "expressionXY.yConfig.axisMode.help": "指标的轴模式", - "expressionXY.yConfig.color.help": "序列的颜色", - "expressionXY.yConfig.fill.help": "填充", - "expressionXY.yConfig.forAccessor.help": "此配置针对的访问器", - "expressionXY.yConfig.help": "配置 xy 图表的 y 轴指标的行为", - "expressionXY.yConfig.icon.help": "用于参考线的可选图标", - "expressionXY.yConfig.iconPosition.help": "参考线图标的位置", - "expressionXY.yConfig.lineStyle.help": "参考线的样式", - "expressionXY.yConfig.lineWidth.help": "参考线的宽度", - "expressionXY.yConfig.textVisibility.help": "参考线上标签的可见性", "fieldFormats.advancedSettings.format.bytesFormat.numeralFormatLinkText": "数值格式", "fieldFormats.advancedSettings.format.bytesFormatText": "“字节”格式的默认{numeralFormatLink}", "fieldFormats.advancedSettings.format.bytesFormatTitle": "字节格式", diff --git a/x-pack/test/functional/apps/lens/group1/smokescreen.ts b/x-pack/test/functional/apps/lens/group1/smokescreen.ts index 2f82218b42a7a..207e546f10d54 100644 --- a/x-pack/test/functional/apps/lens/group1/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/group1/smokescreen.ts @@ -274,13 +274,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); let data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart'); - expect(data?.axes?.y?.[0].title).to.eql(axisTitle); + expect(data?.axes?.y?.[1].title).to.eql(axisTitle); // hide the gridlines await testSubjects.click('lnsshowyLeftAxisGridlines'); data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart'); - expect(data?.axes?.y?.[0].gridlines.length).to.eql(0); + expect(data?.axes?.y?.[1].gridlines.length).to.eql(0); }); it('should transition from a multi-layer stacked bar to donut chart using suggestions', async () => { From 761850e1e8c245cbe8bdc47c7e0cc94b012d586e Mon Sep 17 00:00:00 2001 From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com> Date: Thu, 23 Jun 2022 07:33:53 -0700 Subject: [PATCH 27/54] Fix tag filter button with incorrect styles (#133454) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/rule_tag_filter.test.tsx | 16 ++++- .../rules_list/components/rule_tag_filter.tsx | 62 +++++++++++-------- .../rules_list/components/rules_list.tsx | 4 +- 3 files changed, 55 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.test.tsx index a6b60b1099391..1dc7ef1dc8135 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { EuiFilterButton, EuiSelectable } from '@elastic/eui'; +import { EuiFilterButton, EuiSelectable, EuiFilterGroup } from '@elastic/eui'; import { RuleTagFilter } from './rule_tag_filter'; const onChangeMock = jest.fn(); @@ -74,4 +74,18 @@ describe('rule_tag_filter', () => { tags.length + selectedTags.length ); }); + + it('renders the tag filter with a EuiFilterGroup if isGrouped is false', async () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find(EuiFilterGroup).exists()).toBeTruthy(); + + wrapper.setProps({ + isGrouped: true, + }); + + expect(wrapper.find(EuiFilterGroup).exists()).toBeFalsy(); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx index 47b93ff19c6ea..8f2926a437533 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx @@ -10,6 +10,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { EuiSelectable, EuiFilterButton, + EuiFilterGroup, EuiPopover, EuiSelectableProps, EuiSelectableOption, @@ -19,6 +20,7 @@ import { export interface RuleTagFilterProps { tags: string[]; selectedTags: string[]; + isGrouped?: boolean; // Whether or not this should appear as the child of a EuiFilterGroup isLoading?: boolean; loadingMessage?: EuiSelectableProps['loadingMessage']; noMatchesMessage?: EuiSelectableProps['noMatchesMessage']; @@ -37,6 +39,7 @@ export const RuleTagFilter = (props: RuleTagFilterProps) => { const { tags = [], selectedTags = [], + isGrouped = false, isLoading = false, loadingMessage, noMatchesMessage, @@ -101,33 +104,42 @@ export const RuleTagFilter = (props: RuleTagFilterProps) => { ); }; + const Container = useMemo(() => { + if (isGrouped) { + return React.Fragment; + } + return EuiFilterGroup; + }, [isGrouped]); + return ( - - + - {(list, search) => ( - <> - {search} - - {list} - - )} - - + + {(list, search) => ( + <> + {search} + + {list} + + )} + + + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index 3d54ae8bee5d7..010859538b9ad 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -382,7 +382,9 @@ export const RulesList: React.FunctionComponent = () => { const getRuleTagFilter = () => { if (isRuleTagFilterEnabled) { - return []; + return [ + , + ]; } return []; }; From 8638f12f4f183dd835caf08e1037af6686c6fae7 Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Thu, 23 Jun 2022 09:36:11 -0500 Subject: [PATCH 28/54] Fix small typos in the root md files (#134609) * FAQ.md * STYLEGUIDE.mdx * TYPESCRIPT.md --- FAQ.md | 2 +- STYLEGUIDE.mdx | 56 ++++++++++++++++++++++++++++---------------------- TYPESCRIPT.md | 24 +++++++++++----------- 3 files changed, 45 insertions(+), 37 deletions(-) diff --git a/FAQ.md b/FAQ.md index 26c6df401d150..8a1b1aece9a42 100644 --- a/FAQ.md +++ b/FAQ.md @@ -6,7 +6,7 @@ **Q:** Where do I go for support? **A:** Please join us at [discuss.elastic.co](https://discuss.elastic.co) with questions. Your problem might be a bug, but it might just be a misunderstanding, or a feature we could improve. We're also available on Freenode in #kibana -**Q:** Ok, we talked about it and its definitely a bug +**Q:** Ok, we talked about it, and it's definitely a bug **A:** Doh, ok, let's get that fixed. File an issue on [github.com/elastic/kibana](https://github.com/elastic/kibana). I'd recommend reading the beginning of the CONTRIBUTING.md, just so you know how we'll handle the issue. ### Kibana 3 Migration diff --git a/STYLEGUIDE.mdx b/STYLEGUIDE.mdx index 64278c56ffda5..b06cfa44a4973 100644 --- a/STYLEGUIDE.mdx +++ b/STYLEGUIDE.mdx @@ -38,18 +38,18 @@ and some JavaScript code (check `.eslintrc.js`) is using Prettier to format code can run `node scripts/eslint --fix` to fix linting issues and apply Prettier formatting. We recommend you to enable running ESLint via your IDE. -Whenever possible we are trying to use Prettier and linting over written style guide rules. +Whenever possible we are trying to use Prettier and linting, instead of maintaining a set of written style guide rules. Consider every linting rule and every Prettier rule to be also part of our style guide and disable them only in exceptional cases and ideally leave a comment why they are disabled at that specific place. ## HTML -This part contains style guide rules around general (framework agnostic) HTML usage. +This part contains style guide rules around general (framework-agnostic) HTML usage. ### Camel case `id` and `data-test-subj` -Use camel case for the values of attributes such as `id` and `data-test-subj` selectors. +Use camel case for the values of attributes such as `id` and `data-test-subj` selectors: ```html @@ -73,8 +73,8 @@ buttons.map(btn => ( It's important that when you write CSS/SASS selectors using classes, IDs, and attributes (keeping in mind that we should _never_ use IDs and attributes in our selectors), that the -capitalization in the CSS matches that used in the HTML. HTML and CSS follow different case sensitivity rules, and we can avoid subtle gotchas by ensuring we use the -same capitalization in both of them. +capitalization in the CSS matches that used in the HTML. HTML and CSS follow different case sensitivity rules, +and we can avoid subtle gotchas by ensuring we use the same capitalization in both of them. ### How to generate ids? @@ -143,7 +143,8 @@ API routes must start with the `/api/` path segment, and should be followed by t ### snake_case -Kibana uses `snake_case` for the entire API, just like Elasticsearch. All urls, paths, query string parameters, values, and bodies should be `snake_case` formatted. +Kibana uses `snake_case` for the entire API, just like Elasticsearch. All urls, paths, query string parameters, +values, and bodies should be `snake_case` formatted: _Right:_ @@ -202,7 +203,7 @@ function addBar(foos, foo) { Since TypeScript 3.0 and the introduction of the [`unknown` type](https://mariusschulz.com/blog/the-unknown-type-in-typescript) there are rarely any -reasons to use `any` as a type. Nearly all places of former `any` usage can be replace by either a +reasons to use `any` as a type. Nearly all places of former `any` usage can be replaced by either a generic or `unknown` (in cases the type is really not known). You should always prefer using those mechanisms over using `any`, since they are stricter typed and @@ -215,16 +216,16 @@ linting rule for your plugin via the [`.eslintrc.js`](https://github.com/elastic ### Avoid non-null assertions You should try avoiding non-null assertions (`!.`) wherever possible. By using them you tell -TypeScript, that something is not null even though by it’s type it could be. Usage of non-null -assertions is most often a side-effect of you actually checked that the variable is not `null` -but TypeScript doesn’t correctly carry on that information till the usage of the variable. +TypeScript that something is not `null`, even though by its type it could be. Usage of non-null +assertions is most often a side effect of you actually checked that the variable is not `null` +but TypeScript doesn't correctly carry on that information till the usage of the variable. In most cases it’s possible to replace the non-null assertion by structuring your code/checks slightly different or using [user defined type guards](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards) to properly tell TypeScript what type a variable has. Using non-null assertion increases the risk for future bugs. In case the condition under which we assumed that the -variable can’t be null has changed (potentially even due to changes in compeltely different files), the non-null +variable can’t be `null` has changed (potentially even due to changes in completely different files), the non-null assertion would now wrongly disable proper type checking for us. If you’re not using non-null assertions in your plugin or are starting a new plugin, consider enabling the @@ -266,7 +267,7 @@ function doStuff(val) { ### Use object destructuring -This helps avoid temporary references and helps prevent typo-related bugs. +This helps avoid temporary references and helps prevent typo-related bugs: ```js // best @@ -307,8 +308,8 @@ const second = arr[1]; ### Magic numbers/strings These are numbers (or other values) simply used in line in your code. _Do not -use these_, give them a variable name so they can be understood and changed -easily. +use these_, give them a variable name, so they can be understood and changed +easily: ```js // good @@ -378,7 +379,9 @@ import inSibling from '../foo/child'; #### Avoid export \* in top level index.ts files -The exports in `common/index.ts`, `public/index.ts` and `server/index.ts` dictate a plugin's public API. The public API should be carefully controlled, and using `export *` makes it very easy for a developer working on internal changes to export a new public API unintentionally. +The exports in `common/index.ts`, `public/index.ts` and `server/index.ts` dictate a plugin's public API. +The public API should be carefully controlled, and using `export *` makes it very easy for a developer +working on internal changes to export a new public API unintentionally: ```js // good @@ -399,7 +402,7 @@ by other modules. Even things as simple as a single value should be a module. And _never_ use multiple ternaries together, because they make it more difficult to reason about how different values flow through the conditions -involved. Instead, structure the logic for maximum readability. +involved. Instead, structure the logic for maximum readability: ```js // good, a situation where only 1 ternary is needed @@ -466,7 +469,7 @@ perfect vision and limit yourself to ~15 lines of code per function. ### Use "rest" syntax rather than built-in `arguments` -For expressiveness sake, and so you can be mix dynamic and explicit arguments. +For the sake of expressiveness, and so you can be mix dynamic and explicit arguments: ```js // good @@ -483,7 +486,7 @@ function something(foo) { ### Default argument syntax -Always use the default argument syntax for optional arguments. +Always use the default argument syntax for optional arguments: ```js // good @@ -500,7 +503,7 @@ function foo(options) { } ``` -And put your optional arguments at the end. +And put your optional arguments at the end: ```js // good @@ -518,7 +521,7 @@ function foo(options = {}, bar) { For trivial examples (like the one that follows), thunks will seem like overkill, but they encourage isolating the implementation details of a closure -from the business logic of the calling code. +from the business logic of the calling code: ```js // good @@ -621,9 +624,13 @@ by your code until the circular dependencies on these have been solved. ## SASS files -When writing a new component, create a sibling SASS file of the same name and import directly into the **top** of the JS/TS component file. Doing so ensures the styles are never separated or lost on import and allows for better modularization (smaller individual plugin asset footprint). +When writing a new component, create a sibling SASS file of the same name and import +directly into the **top** of the JS/TS component file. +Doing so ensures the styles are never separated or lost on import and allows +for better modularization (smaller individual plugin asset footprint). -All SASS (.scss) files will automatically build with the [EUI](https://elastic.github.io/eui/#/guidelines/sass) & Kibana invisibles (SASS variables, mixins, functions) from the [`globals_[theme].scss` file](src/core/public/core_app/styles/_globals_v8light.scss). +All SASS (.scss) files will automatically build with the [EUI](https://elastic.github.io/eui/#/guidelines/sass) +& Kibana invisibles (SASS variables, mixins, functions) from the [`globals_[theme].scss` file](src/core/public/core_app/styles/_globals_v8light.scss). While the styles for this component will only be loaded if the component exists on the page, the styles **will** be global and so it is recommended to use a three letter prefix on your @@ -656,11 +663,12 @@ The following style guide rules are specific for working with the React framewor ### Prefer reactDirective over react-component -When using `ngReact` to embed your react components inside Angular HTML, prefer the +When using `ngReact` to embed your React components inside Angular HTML, prefer the `reactDirective` service over the `react-component` directive. You can read more about these two ngReact methods [here](https://github.com/ngReact/ngReact#features). -Using `react-component` means adding a bunch of components into angular, while `reactDirective` keeps them isolated, and is also a more succinct syntax. +Using `react-component` means adding a bunch of components into angular, while `reactDirective` keeps them isolated, +and is also a more succinct syntax: **Good:** diff --git a/TYPESCRIPT.md b/TYPESCRIPT.md index ae23768558f9d..9edd11ff4f1b0 100644 --- a/TYPESCRIPT.md +++ b/TYPESCRIPT.md @@ -4,7 +4,7 @@ To convert existing code over to TypeScript: 1. rename the file from `.js` to either `.ts` (if there is no html or jsx in the file) or `.tsx` (if there is). -2. Ensure eslint is running and installed in the IDE of your choice. There will usually be some linter errors after the file rename. +2. Ensure eslint is running and installed in the IDE of your choice. There will usually be some linter errors after the file rename. 3. Auto-fix what you can. This will save you a lot of time! VSCode can be set to auto fix eslint errors when files are saved. ### How to fix common TypeScript errors @@ -14,7 +14,7 @@ The first thing that will probably happen when you convert a `.js` file in our s #### EUI component is missing types 1. Check https://github.com/elastic/eui/issues/256 to see if they know it’s missing, if it’s not on there, add it. -2. Temporarily get around the issue by adding the missing type in the `typings/@elastic/eui/index.d.ts` file. Bonus points if you write a PR yourself to the EUI repo to add the types, but having them available back in Kibana will take some time, as a new EUI release will need to be generated, then that new release pointed to in Kibana. Best, to make forward progress, to do a temporary workaround. +2. Temporarily get around the issue by adding the missing type in the `typings/@elastic/eui/index.d.ts` file. Bonus points if you write a PR yourself to the EUI repo to add the types, but having them available back in Kibana will take some time, as a new EUI release will need to be generated, then that new release pointed to in Kibana. Best, to make forward progress, to do a temporary workaround. ```ts // typings/@elastic/eui/index.d.ts @@ -61,8 +61,8 @@ declare module '@elastic/eui' { 1. Open up the file and see how easy it would be to convert to TypeScript. 2. If it's very straightforward, go for it. -3. If it's not and you wish to stay focused on your own PR, get around the error by adding a type definition file in the same folder as the dependency, with the same name. -4. Minimally you will need to type what you are using in your PR. No need to go crazy to fully type the thing or you might be there for a while depending on what's available. +3. If it's not, and you wish to stay focused on your own PR, get around the error by adding a type definition file in the same folder as the dependency, with the same name. +4. Minimally you will need to type what you are using in your PR. For example: @@ -107,15 +107,15 @@ Use the version number that we have installed in package.json. This may not alwa If that happens, just pick the closest one. -If yarn doesn't find the module it may not have types. For example, our `rison_node` package doesn't have types. In this case you have a few options: +If yarn doesn't find the module it may not have types. For example, our `rison_node` package doesn't have types. In this case you have a few options: 1. Contribute types into the DefinitelyTyped repo itself, or -2. Create a top level `types` folder and point to that in the tsconfig. For example, Infra team already handled this for `rison_node` and added: `x-pack/legacy/plugins/infra/types/rison_node.d.ts`. Other code uses it too so we will need to pull it up. Or, +2. Create a top level `types` folder and point to that in the tsconfig. For example, Infra team already handled this for `rison_node` and added: `x-pack/legacy/plugins/infra/types/rison_node.d.ts`. Other code uses it too, so we will need to pull it up. Or, 3. Add a `// @ts-ignore` line above the import. This should be used minimally, the above options are better. However, sometimes you have to resort to this method. ### TypeScripting react files -React has it's own concept of runtime types via `proptypes`. TypeScript gives you compile time types so I prefer those. +React has its own concept of runtime types via `proptypes`. TypeScript gives you compile time types so I prefer those. Before: ```jsx @@ -159,11 +159,11 @@ interface State { } ``` -Note that the name of `Props` and `State` doesn't matter, the order does. If you are exporting those interfaces to be used elsewhere, you probably should give them more fleshed out names, such as `ButtonProps` and `ButtonState`. +Note that the name of `Props` and `State` doesn't matter, the order does. If you are exporting those interfaces to be used elsewhere, you probably should give them more fleshed out names, such as `ButtonProps` and `ButtonState`. ### Typing functions -In react proptypes, we often will use `PropTypes.func`. In TypeScript, a function is `() => void`, or you can more fully flesh it out, for example: +In react proptypes, we often will use `PropTypes.func`. In TypeScript, a function is `() => void`, or you can more fully flesh it out, for example: - `(inputParamName: string) => string` - `(newLanguage: string) => void` @@ -186,7 +186,7 @@ function ({ title, description }: {title: string, description: string}) { ... } -or, use an interface +// or use an interface interface Options { title: string; @@ -200,9 +200,9 @@ function ({ title, description }: Options) { ## Use `any` as little as possible -Using any is sometimes valid, but should rarely be used, even if to make quicker progress. Even `Unknown` is better than using `any` if you aren't sure of an input parameter. +Using any is sometimes valid, but should rarely be used, even if to make quicker progress. Even `unknown` is better than using `any` if you aren't sure of an input parameter. -If you use a variable that isn't initially defined, you should give it a type or it will be `any` by default (and strangely this isn't a warning, even though I think it should be) +If you use a variable that isn't initially defined, you should give it a type, or it will be `any` by default (and strangely this isn't a warning, even though I think it should be) Before - `color` will be type `any`: ```js From 31db97850d7771118ea2c1af7e281496098e5524 Mon Sep 17 00:00:00 2001 From: Pete Hampton Date: Thu, 23 Jun 2022 15:38:29 +0100 Subject: [PATCH 29/54] [Telemetry] Allow message field though. (#134901) --- .../server/lib/telemetry/filterlists/endpoint_alerts.ts | 1 + .../security_solution/server/lib/telemetry/sender.test.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/endpoint_alerts.ts b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/endpoint_alerts.ts index 41394144e9c66..34777cc225260 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/endpoint_alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/endpoint_alerts.ts @@ -75,6 +75,7 @@ const allowlistBaseEventFields: AllowlistFields = { quarantine_message: true, }, }, + message: true, process: { parent: baseAllowlistFields, ...baseAllowlistFields, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index 8e36dfd9268a5..dbe9a2f1f2fa6 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -86,6 +86,7 @@ describe('TelemetryEventsSender', () => { }, something_else: 'nope', }, + message: 'Malicious Behavior Detection Alert: Regsvr32 with Unusual Arguments', process: { name: 'foo.exe', nope: 'nope', @@ -164,6 +165,7 @@ describe('TelemetryEventsSender', () => { name: 'windows', }, }, + message: 'Malicious Behavior Detection Alert: Regsvr32 with Unusual Arguments', process: { name: 'foo.exe', working_directory: '/some/usr/dir', From 69caa311bbcbb73588422d4f8e2b9db7f13c78af Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Thu, 23 Jun 2022 15:51:59 +0100 Subject: [PATCH 30/54] [Fleet] Download Elastic GPG key during build (#134861) * add build step to download gpg key * add gpg path to config * add getGpgKey method * getGpgKey reads config directly * improve logging * return undefined on error * log error code instead of msg * perform checksum check on GPG key * fail build if GPG key download fails --- src/dev/build/build_distributables.ts | 1 + .../tasks/fleet_download_elastic_gpg_key.ts | 40 +++++++++++++ src/dev/build/tasks/index.ts | 5 +- x-pack/plugins/fleet/common/types/index.ts | 3 + x-pack/plugins/fleet/server/index.ts | 4 ++ .../epm/packages/package_verification.test.ts | 58 +++++++++++++++++++ .../epm/packages/package_verification.ts | 42 ++++++++++++++ 7 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 src/dev/build/tasks/fleet_download_elastic_gpg_key.ts create mode 100644 x-pack/plugins/fleet/server/services/epm/packages/package_verification.test.ts create mode 100644 x-pack/plugins/fleet/server/services/epm/packages/package_verification.ts diff --git a/src/dev/build/build_distributables.ts b/src/dev/build/build_distributables.ts index 0a3db5dc36d07..876dbe220de6a 100644 --- a/src/dev/build/build_distributables.ts +++ b/src/dev/build/build_distributables.ts @@ -88,6 +88,7 @@ export async function buildDistributables(log: ToolingLog, options: BuildOptions await run(Tasks.CleanTypescript); await run(Tasks.CleanExtraFilesFromModules); await run(Tasks.CleanEmptyFolders); + await run(Tasks.FleetDownloadElasticGpgKey); await run(Tasks.BundleFleetPackages); } diff --git a/src/dev/build/tasks/fleet_download_elastic_gpg_key.ts b/src/dev/build/tasks/fleet_download_elastic_gpg_key.ts new file mode 100644 index 0000000000000..8d52c9166d25c --- /dev/null +++ b/src/dev/build/tasks/fleet_download_elastic_gpg_key.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Task, downloadToDisk } from '../lib'; + +const BUNDLED_KEYS_DIR = 'x-pack/plugins/fleet/target/keys'; +const ARTIFACTS_URL = 'https://artifacts.elastic.co/'; +const GPG_KEY_NAME = 'GPG-KEY-elasticsearch'; +const GPG_KEY_SHA512 = + '84ee193cc337344d9a7da9021daf3f5ede83f5f1ab049d169f3634921529dcd096abf7a91eec7f26f3a6913e5e38f88f69a5e2ce79ad155d46edc75705a648c6'; + +export const FleetDownloadElasticGpgKey: Task = { + description: 'Downloading Elastic GPG key for Fleet', + + async run(config, log, build) { + const gpgKeyUrl = ARTIFACTS_URL + GPG_KEY_NAME; + const destination = build.resolvePath(BUNDLED_KEYS_DIR, GPG_KEY_NAME); + log.info(`Downloading Elastic GPG key from ${gpgKeyUrl} to ${destination}`); + + try { + await downloadToDisk({ + log, + url: gpgKeyUrl, + destination, + shaChecksum: GPG_KEY_SHA512, + shaAlgorithm: 'sha512', + skipChecksumCheck: false, + maxAttempts: 3, + }); + } catch (error) { + log.error(`Error downloading Elastic GPG key from ${gpgKeyUrl} to ${destination}`); + throw error; + } + }, +}; diff --git a/src/dev/build/tasks/index.ts b/src/dev/build/tasks/index.ts index f158634829100..94e6d107ff8da 100644 --- a/src/dev/build/tasks/index.ts +++ b/src/dev/build/tasks/index.ts @@ -7,8 +7,8 @@ */ export * from './bin'; -export * from './build_kibana_platform_plugins'; export * from './build_kibana_example_plugins'; +export * from './build_kibana_platform_plugins'; export * from './build_packages_task'; export * from './bundle_fleet_packages'; export * from './clean_tasks'; @@ -18,6 +18,7 @@ export * from './create_archives_task'; export * from './create_empty_dirs_and_files_task'; export * from './create_readme_task'; export * from './download_cloud_dependencies'; +export * from './fleet_download_elastic_gpg_key'; export * from './generate_packages_optimized_assets'; export * from './install_dependencies_task'; export * from './license_file_task'; @@ -27,11 +28,11 @@ export * from './os_packages'; export * from './package_json'; export * from './patch_native_modules_task'; export * from './path_length_task'; +export * from './replace_favicon'; export * from './transpile_babel_task'; export * from './uuid_verification_task'; export * from './verify_env_task'; export * from './write_sha_sums_task'; -export * from './replace_favicon'; // @ts-expect-error this module can't be TS because it ends up pulling x-pack into Kibana export { InstallChromium } from './install_chromium'; diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index ce1cb5a294f80..9f88d9b231854 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -33,6 +33,9 @@ export interface FleetConfigType { outputs?: PreconfiguredOutput[]; agentIdVerificationEnabled?: boolean; enableExperimental?: string[]; + packageVerification?: { + gpgKeyPath?: string; + }; developer?: { disableRegistryVersionCheck?: boolean; allowAgentUpgradeSourceUri?: boolean; diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index cd7c2e5ce8ff6..443bd00a3cf19 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -47,6 +47,7 @@ export type { export { AgentNotFoundError, FleetUnauthorizedError } from './errors'; const DEFAULT_BUNDLED_PACKAGE_LOCATION = path.join(__dirname, '../target/bundled_packages'); +const DEFAULT_GPG_KEY_PATH = path.join(__dirname, '../target/keys/GPG-KEY-elasticsearch'); export const config: PluginConfigDescriptor = { exposeToBrowser: { @@ -143,6 +144,9 @@ export const config: PluginConfigDescriptor = { allowAgentUpgradeSourceUri: schema.boolean({ defaultValue: false }), bundledPackageLocation: schema.string({ defaultValue: DEFAULT_BUNDLED_PACKAGE_LOCATION }), }), + packageVerification: schema.object({ + gpgKeyPath: schema.string({ defaultValue: DEFAULT_GPG_KEY_PATH }), + }), /** * For internal use. A list of string values (comma delimited) that will enable experimental * type of functionality that is not yet released. diff --git a/x-pack/plugins/fleet/server/services/epm/packages/package_verification.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/package_verification.test.ts new file mode 100644 index 0000000000000..7bb5ee2ba7aff --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/packages/package_verification.test.ts @@ -0,0 +1,58 @@ +/* + * 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 { readFile } from 'fs/promises'; + +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; + +const mockLoggerFactory = loggingSystemMock.create(); +const mockLogger = mockLoggerFactory.get('mock logger'); +import { getGpgKeyOrUndefined, _readGpgKey } from './package_verification'; + +const mockGetConfig = jest.fn(); +jest.mock('../../app_context', () => ({ + appContextService: { + getConfig: () => mockGetConfig(), + getLogger: () => mockLogger, + }, +})); + +jest.mock('fs/promises', () => ({ + readFile: jest.fn(), +})); + +const mockedReadFile = readFile as jest.MockedFunction; + +beforeEach(() => { + jest.resetAllMocks(); +}); +describe('getGpgKeyOrUndefined', () => { + it('should cache the gpg key after reading file once', async () => { + const keyContent = 'this is the gpg key'; + mockedReadFile.mockResolvedValue(Buffer.from(keyContent)); + mockGetConfig.mockReturnValue({ packageVerification: { gpgKeyPath: 'somePath' } }); + expect(await getGpgKeyOrUndefined()).toEqual(keyContent); + expect(await getGpgKeyOrUndefined()).toEqual(keyContent); + expect(mockedReadFile).toHaveBeenCalledWith('somePath'); + expect(mockedReadFile).toHaveBeenCalledTimes(1); + }); +}); + +describe('_readGpgKey', () => { + it('should return undefined if the key file isnt configured', async () => { + mockedReadFile.mockResolvedValue(Buffer.from('this is the gpg key')); + mockGetConfig.mockReturnValue({ packageVerification: {} }); + + expect(await _readGpgKey()).toEqual(undefined); + }); + it('should return undefined if there is an error reading the file', async () => { + mockedReadFile.mockRejectedValue(new Error('some error')); + mockGetConfig.mockReturnValue({ packageVerification: { gpgKeyPath: 'somePath' } }); + expect(await _readGpgKey()).toEqual(undefined); + expect(mockedReadFile).toHaveBeenCalledWith('somePath'); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/package_verification.ts b/x-pack/plugins/fleet/server/services/epm/packages/package_verification.ts new file mode 100644 index 0000000000000..9f1a07243a7d3 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/packages/package_verification.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 { readFile } from 'fs/promises'; + +import { appContextService } from '../../app_context'; + +let cachedKey: string | undefined | null = null; + +export async function getGpgKeyOrUndefined(): Promise { + if (cachedKey !== null) return cachedKey; + + cachedKey = await _readGpgKey(); + return cachedKey; +} + +export async function _readGpgKey(): Promise { + const config = appContextService.getConfig(); + const logger = appContextService.getLogger(); + const gpgKeyPath = config?.packageVerification?.gpgKeyPath; + + if (!gpgKeyPath) { + logger.warn('GPG key path not configured at "xpack.fleet.packageVerification.gpgKeyPath"'); + return undefined; + } + + let buffer: Buffer; + try { + buffer = await readFile(gpgKeyPath); + } catch (e) { + logger.warn(`Unable to retrieve GPG key from '${gpgKeyPath}': ${e.code}`); + return undefined; + } + + const key = buffer.toString(); + cachedKey = key; + return key; +} From d61253aa0e00af5cbe24d8eb4fd96c6863d1c6fa Mon Sep 17 00:00:00 2001 From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> Date: Thu, 23 Jun 2022 16:58:23 +0200 Subject: [PATCH 31/54] limit agent list total count to 10k (#135026) --- .../fleet/sections/agents/agent_list_page/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index 9ab39134c2cdc..bc55e477b9993 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -626,7 +626,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { pagination={{ pageIndex: pagination.currentPage - 1, pageSize: pagination.pageSize, - totalItemCount: totalAgents, + totalItemCount: Math.min(totalAgents, SO_SEARCH_LIMIT), pageSizeOptions, }} isSelectable={true} From 7fb3931bb1c29c43863f6c20539d53dd4b83f7f6 Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Thu, 23 Jun 2022 11:00:08 -0400 Subject: [PATCH 32/54] Remove beta messaging from Logstash output flyout (#135028) --- .../components/edit_output_flyout/index.tsx | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx index 09bd7d2280756..3d0e9b2801394 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx @@ -46,7 +46,7 @@ export interface EditOutputFlyoutProps { const OUTPUT_TYPE_OPTIONS = [ { value: 'elasticsearch', text: 'Elasticsearch' }, - { value: 'logstash', text: 'Logstash (beta)' }, + { value: 'logstash', text: 'Logstash' }, ]; export const EditOutputFlyout: React.FunctionComponent = ({ @@ -134,24 +134,6 @@ export const EditOutputFlyout: React.FunctionComponent = defaultMessage="Type" /> } - helpText={ - isLogstashOutput && ( - - - - ), - }} - /> - ) - } > Date: Thu, 23 Jun 2022 16:00:48 +0100 Subject: [PATCH 33/54] [QA][Code Coverage] Add meta data links ci runs. (#134731) * [QA][Code Coverage] Add a link to discover pinned to the current build. When personnel other than myself wish to visually verify code coverage, this link will be available in the ci output. * Drop single quotes. * Add annotation per J Budz. * Fixup text. * Drop debug. --- .buildkite/pipelines/code_coverage/daily.yml | 4 +- .../scripts/steps/code_coverage/ingest.sh | 50 +++++++++++++++---- .../code_coverage/reporting/ingestData.sh | 17 +++++++ 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/.buildkite/pipelines/code_coverage/daily.yml b/.buildkite/pipelines/code_coverage/daily.yml index c9bb675ab40cf..dd8496bac40ca 100644 --- a/.buildkite/pipelines/code_coverage/daily.yml +++ b/.buildkite/pipelines/code_coverage/daily.yml @@ -13,7 +13,7 @@ steps: queue: kibana-default env: FTR_CONFIGS_DEPS: '' -# LIMIT_CONFIG_TYPE: 'unit,functional,integration' + # LIMIT_CONFIG_TYPE: 'unit,functional,integration' LIMIT_CONFIG_TYPE: 'unit,integration' JEST_UNIT_SCRIPT: '.buildkite/scripts/steps/code_coverage/jest.sh' JEST_INTEGRATION_SCRIPT: '.buildkite/scripts/steps/code_coverage/jest_integration.sh' @@ -26,6 +26,6 @@ steps: depends_on: - jest - jest-integration -# - ftr-configs + # - ftr-configs timeout_in_minutes: 30 key: ingest diff --git a/.buildkite/scripts/steps/code_coverage/ingest.sh b/.buildkite/scripts/steps/code_coverage/ingest.sh index 34d54c4d61b09..6632009346481 100755 --- a/.buildkite/scripts/steps/code_coverage/ingest.sh +++ b/.buildkite/scripts/steps/code_coverage/ingest.sh @@ -17,14 +17,16 @@ export HOST_FROM_VAULT TIME_STAMP=$(date +"%Y-%m-%dT%H:%M:00Z") export TIME_STAMP -echo "--- Download previous git sha" -.buildkite/scripts/steps/code_coverage/reporting/downloadPrevSha.sh -PREVIOUS_SHA=$(cat downloaded_previous.txt) +.buildkite/scripts/bootstrap.sh -echo "--- Upload new git sha" -.buildkite/scripts/steps/code_coverage/reporting/uploadPrevSha.sh +revolveBuildHashes() { + echo "--- Download previous git sha" + .buildkite/scripts/steps/code_coverage/reporting/downloadPrevSha.sh + PREVIOUS_SHA=$(cat downloaded_previous.txt) -.buildkite/scripts/bootstrap.sh + echo "--- Upload new git sha" + .buildkite/scripts/steps/code_coverage/reporting/uploadPrevSha.sh +} collectRan() { buildkite-agent artifact download target/ran_files/* . @@ -59,9 +61,9 @@ archiveReports() { local xs=("$@") for x in "${xs[@]}"; do echo "### Collect and Upload for: ${x}" -# fileHeads "target/file-heads-archive-reports-for-${x}.txt" "target/kibana-coverage/${x}" -# dirListing "target/dir-listing-${x}-combined-during-archiveReports.txt" target/kibana-coverage/${x}-combined -# dirListing "target/dir-listing-${x}-during-archiveReports.txt" target/kibana-coverage/${x} + # fileHeads "target/file-heads-archive-reports-for-${x}.txt" "target/kibana-coverage/${x}" + # dirListing "target/dir-listing-${x}-combined-during-archiveReports.txt" target/kibana-coverage/${x}-combined + # dirListing "target/dir-listing-${x}-during-archiveReports.txt" target/kibana-coverage/${x} collectAndUpload "target/kibana-coverage/${x}/kibana-${x}-coverage.tar.gz" "target/kibana-coverage/${x}-combined" done } @@ -86,20 +88,49 @@ mergeAll() { done } +annotateForStaticSite() { + local xs=("$@") + local markdownLinks=() + + OLDIFS="${IFS}" + IFS=$'\n' + + for x in "${xs[@]}"; do + markdownLinks+=(" - [$x](https://kibana-coverage.elastic.dev/${TIME_STAMP}/${x}-combined/index.html)") + done + + content=$( + cat <<-EOF +### Browse the Code Coverage Static Site + +_Links are pinned to the current build number._ + +${markdownLinks[*]} +EOF + ) + + IFS="${OLDIFS}" + + buildkite-agent annotate --style "info" --context 'ctx-coverage-static-site' "${content}" +} + modularize() { collectRan if [ -d target/ran_files ]; then + revolveBuildHashes uniqueifyRanConfigs "${ran[@]}" fetchArtifacts "${uniqRanConfigs[@]}" mergeAll "${uniqRanConfigs[@]}" archiveReports "${uniqRanConfigs[@]}" .buildkite/scripts/steps/code_coverage/reporting/prokLinks.sh "${uniqRanConfigs[@]}" .buildkite/scripts/steps/code_coverage/reporting/uploadStaticSite.sh "${uniqRanConfigs[@]}" + annotateForStaticSite "${uniqRanConfigs[@]}" .buildkite/scripts/steps/code_coverage/reporting/collectVcsInfo.sh source .buildkite/scripts/steps/code_coverage/reporting/ingestData.sh 'elastic+kibana+code-coverage' \ "${BUILDKITE_BUILD_NUMBER}" "${BUILDKITE_BUILD_URL}" "${PREVIOUS_SHA}" \ 'src/dev/code_coverage/ingest_coverage/team_assignment/team_assignments.txt' ingestModular "${uniqRanConfigs[@]}" + annotateForKibanaLinks else echo "--- Found zero configs that ran, cancelling ingestion." exit 11 @@ -107,4 +138,3 @@ modularize() { } modularize -echo "### unique ran configs: ${uniqRanConfigs[*]}" diff --git a/.buildkite/scripts/steps/code_coverage/reporting/ingestData.sh b/.buildkite/scripts/steps/code_coverage/reporting/ingestData.sh index 7eac3727cfc60..ae0d6c5cda84e 100755 --- a/.buildkite/scripts/steps/code_coverage/reporting/ingestData.sh +++ b/.buildkite/scripts/steps/code_coverage/reporting/ingestData.sh @@ -29,6 +29,23 @@ echo "### debug TEAM_ASSIGN_PATH: ${TEAM_ASSIGN_PATH}" BUFFER_SIZE=500 export BUFFER_SIZE +annotateForKibanaLinks() { + local currentBuildNumber="$BUILDKITE_BUILD_NUMBER" + local coverageUrl="https://kibana-stats.elastic.dev/app/discover#/?_g=(filters:!(),query:(language:kuery,query:''),refreshInterval:(pause:!t,value:0),time:(from:now-7d,to:now))&_a=(columns:!(),filters:!(),hideChart:!f,index:'64419790-4218-11ea-b2d8-81bcbf78dfcb',interval:auto,query:(language:kuery,query:'BUILD_ID%20:%20${currentBuildNumber}'),sort:!(!('@timestamp',desc)))" + local totalCoverageUrl="https://kibana-stats.elastic.dev/app/discover#/?_g=(filters:!(),query:(language:kuery,query:''),refreshInterval:(pause:!t,value:0),time:(from:now-7d,to:now))&_a=(columns:!(),filters:!(),hideChart:!f,index:d78f9120-4218-11ea-b2d8-81bcbf78dfcb,interval:auto,query:(language:kuery,query:'BUILD_ID%20:%20${currentBuildNumber}'),sort:!(!('@timestamp',desc)))" + + cat < Date: Thu, 23 Jun 2022 17:10:51 +0200 Subject: [PATCH 34/54] [SecuritySolution] Migrate (some) timlines tests to testing-library (#134688) * test: migrate some tests to testing-library * test: re-add some data-test-sub attributes * test: re-add some more data-test-subj attributes * test: test: migrate some more tests to testing-library * test: update snapshots --- .../network/components/direction/index.tsx | 1 - .../__snapshots__/index.test.tsx.snap | 129 +- .../source_destination/index.test.tsx | 143 +- .../components/source_destination/network.tsx | 6 +- .../source_destination_arrows.tsx | 21 +- .../source_destination_ip.test.tsx | 116 +- .../source_destination_ip.tsx | 2 +- .../certificate_fingerprint/index.test.tsx | 26 +- .../certificate_fingerprint/index.tsx | 2 +- .../components/duration/index.test.tsx | 8 +- .../edit_data_provider/index.test.tsx | 85 +- .../components/edit_data_provider/index.tsx | 1 - .../field_renderers.test.tsx.snap | 448 ++- .../field_renderers/field_renderers.test.tsx | 236 +- .../field_renderers/field_renderers.tsx | 15 +- .../flyout/__snapshots__/index.test.tsx.snap | 426 ++- .../flyout/add_timeline_button/index.test.tsx | 108 +- .../flyout/add_to_case_button/index.test.tsx | 15 +- .../flyout/bottom_bar/index.test.tsx | 26 +- .../components/flyout/header/index.test.tsx | 65 +- .../components/flyout/index.test.tsx | 49 +- .../components/formatted_ip/index.test.tsx | 90 +- .../components/ja3_fingerprint/index.test.tsx | 20 +- .../components/ja3_fingerprint/index.tsx | 4 +- .../netflow/__snapshots__/index.test.tsx.snap | 2936 +++++++++++++- .../components/netflow/index.test.tsx | 220 +- .../duration_event_start_end.tsx | 3 - .../netflow/netflow_columns/user_process.tsx | 1 - .../netflow_row_renderer.test.tsx.snap | 3379 ++++++++++++++++- .../netflow/netflow_row_renderer.test.tsx | 13 +- 30 files changed, 7204 insertions(+), 1390 deletions(-) diff --git a/x-pack/plugins/security_solution/public/network/components/direction/index.tsx b/x-pack/plugins/security_solution/public/network/components/direction/index.tsx index d3661f390b2d6..ce4052ed8bd74 100644 --- a/x-pack/plugins/security_solution/public/network/components/direction/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/direction/index.tsx @@ -64,7 +64,6 @@ export const DirectionBadge = React.memo<{ }>(({ contextId, eventId, direction, isDraggable }) => ( - -
-`; +exports[`SourceDestination renders correctly against snapshot 1`] = `[Function]`; diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx index ee35ebb74767f..3eea6972971a7 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx @@ -6,17 +6,15 @@ */ import numeral from '@elastic/numeral'; -import { shallow } from 'enzyme'; import { get } from 'lodash/fp'; import React from 'react'; +import { render, screen, within } from '@testing-library/react'; -import { removeExternalLinkText } from '@kbn/securitysolution-io-ts-utils'; import { asArrayIfExists } from '../../../common/lib/helpers'; import { getMockNetflowData } from '../../../common/mock'; import '../../../common/mock/match_media'; import { TestProviders } from '../../../common/mock/test_providers'; import { ID_FIELD_NAME } from '../../../common/components/event_details/event_id'; -import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../ip'; import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME } from '../port/helpers'; import { @@ -121,29 +119,25 @@ jest.mock('react-router-dom', () => { }); describe('SourceDestination', () => { - const mount = useMountAppended(); - test('renders correctly against snapshot', () => { - const wrapper = shallow(
{getSourceDestinationInstance()}
); - expect(wrapper).toMatchSnapshot(); + const { asFragment } = render({getSourceDestinationInstance()}); + expect(asFragment).toMatchSnapshot(); }); test('it renders a destination label', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="destination-label"]').first().text()).toEqual( - i18n.DESTINATION - ); + expect(screen.getByText(i18n.DESTINATION)).toBeInTheDocument(); }); test('it renders destination.bytes', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="destination-bytes"]').first().text()).toEqual('40B'); + expect(screen.getByText('40B')).toBeInTheDocument(); }); test('it renders percent destination.bytes', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); const destinationBytes = asArrayIfExists( get(DESTINATION_BYTES_FIELD_NAME, getMockNetflowData()) ); @@ -153,127 +147,116 @@ describe('SourceDestination', () => { percent = `(${numeral((destinationBytes[0] / sumBytes[0]) * 100).format('0.00')}%)`; } - expect(wrapper.find('[data-test-subj="destination-bytes-percent"]').first().text()).toEqual( - percent - ); + expect(screen.getByText(percent)).toBeInTheDocument(); }); test('it renders destination.geo.continent_name', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect( - wrapper.find('[data-test-subj="destination.geo.continent_name"]').first().text() - ).toEqual('North America'); + expect(screen.getByTestId('draggable-content-destination.geo.continent_name').textContent).toBe( + 'North America' + ); }); test('it renders destination.geo.country_name', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="destination.geo.country_name"]').first().text()).toEqual( + expect(screen.getByTestId('draggable-content-destination.geo.country_name').textContent).toBe( 'United States' ); }); test('it renders destination.geo.country_iso_code', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); expect( - wrapper.find('[data-test-subj="destination.geo.country_iso_code"]').first().text() - ).toEqual('US'); + screen.getByTestId('draggable-content-destination.geo.country_iso_code').textContent + ).toBe('US'); }); test('it renders destination.geo.region_name', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="destination.geo.region_name"]').first().text()).toEqual( + expect(screen.getByTestId('draggable-content-destination.geo.region_name').textContent).toBe( 'New York' ); }); test('it renders destination.geo.city_name', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="destination.geo.city_name"]').first().text()).toEqual( + expect(screen.getByTestId('draggable-content-destination.geo.city_name').textContent).toBe( 'New York' ); }); test('it renders the destination ip and port, separated with a colon', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect( - removeExternalLinkText( - wrapper.find('[data-test-subj="destination-ip-and-port"]').first().text() - ) - ).toContain('10.1.2.3:80'); + expect(screen.getByTestId('destination-ip-badge').textContent).toContain('10.1.2.3:80'); }); test('it renders destination.packets', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="destination-packets"]').first().text()).toEqual('1 pkts'); + expect(screen.getByText('1 pkts')).toBeInTheDocument(); }); test('it hyperlinks links destination.port to an external service that describes the purpose of the port', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); expect( - wrapper - .find('[data-test-subj="destination-ip-and-port"]') - .find('[data-test-subj="port-or-service-name-link"]') - .first() - .props().href - ).toEqual( + within(screen.getByTestId('destination-ip-group')).getByTestId('port-or-service-name-link') + ).toHaveAttribute( + 'href', 'https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=80' ); }); test('it renders network.bytes', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="network-bytes"]').first().text()).toEqual('100B'); + expect(screen.getByText('100B')).toBeInTheDocument(); }); test('it renders network.community_id', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="network-community-id"]').first().text()).toEqual( - 'we.live.in.a' - ); + expect(screen.getByText('we.live.in.a')).toBeInTheDocument(); }); test('it renders network.direction', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="network-direction"]').first().text()).toEqual('outgoing'); + expect(screen.getByText('outgoing')).toBeInTheDocument(); }); test('it renders network.packets', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="network-packets"]').first().text()).toEqual('3 pkts'); + expect(screen.getByText('3 pkts')).toBeInTheDocument(); }); test('it renders network.protocol', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="network-protocol"]').first().text()).toEqual('http'); + expect(screen.getByText('http')).toBeInTheDocument(); }); test('it renders a source label', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="source-label"]').first().text()).toEqual(i18n.SOURCE); + expect(screen.getByText(i18n.SOURCE)).toBeInTheDocument(); }); test('it renders source.bytes', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="source-bytes"]').first().text()).toEqual('60B'); + expect(screen.getByText('60B')).toBeInTheDocument(); }); test('it renders percent source.bytes', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); const sourceBytes = asArrayIfExists(get(SOURCE_BYTES_FIELD_NAME, getMockNetflowData())); const sumBytes = asArrayIfExists(get(NETWORK_BYTES_FIELD_NAME, getMockNetflowData())); let percent = ''; @@ -281,66 +264,64 @@ describe('SourceDestination', () => { percent = `(${numeral((sourceBytes[0] / sumBytes[0]) * 100).format('0.00')}%)`; } - expect(wrapper.find('[data-test-subj="source-bytes-percent"]').first().text()).toEqual(percent); + expect(screen.getByText(percent)).toBeInTheDocument(); }); test('it renders source.geo.continent_name', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="source.geo.continent_name"]').first().text()).toEqual( + expect(screen.getByTestId('draggable-content-source.geo.continent_name').textContent).toBe( 'North America' ); }); test('it renders source.geo.country_name', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="source.geo.country_name"]').first().text()).toEqual( + expect(screen.getByTestId('draggable-content-source.geo.country_name').textContent).toBe( 'United States' ); }); test('it renders source.geo.country_iso_code', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="source.geo.country_iso_code"]').first().text()).toEqual( + expect(screen.getByTestId('draggable-content-source.geo.country_iso_code').textContent).toBe( 'US' ); }); test('it renders source.geo.region_name', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="source.geo.region_name"]').first().text()).toEqual( + expect(screen.getByTestId('draggable-content-source.geo.region_name').textContent).toBe( 'Georgia' ); }); test('it renders source.geo.city_name', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="source.geo.city_name"]').first().text()).toEqual( + expect(screen.getByTestId('draggable-content-source.geo.city_name').textContent).toBe( 'Atlanta' ); }); test('it renders the source ip and port, separated with a colon', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect( - removeExternalLinkText(wrapper.find('[data-test-subj="source-ip-and-port"]').first().text()) - ).toContain('192.168.1.2:9987'); + expect(screen.getByTestId('source-ip-badge').textContent).toContain('192.168.1.2:9987'); }); test('it renders source.packets', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="source-packets"]').first().text()).toEqual('2 pkts'); + expect(screen.getByText('2 pkts')).toBeInTheDocument(); }); test('it renders network.transport', () => { - const wrapper = mount({getSourceDestinationInstance()}); + render({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="network-transport"]').first().text()).toEqual('tcp'); + expect(screen.getByText('tcp')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/network.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/network.tsx index e2f2d8d725181..727818ebad2a8 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/network.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/network.tsx @@ -80,7 +80,6 @@ export const Network = React.memo<{ - + @@ -123,7 +122,7 @@ export const Network = React.memo<{ value={p} > - {`${p} ${i18n.PACKETS}`} + {`${p} ${i18n.PACKETS}`} @@ -152,7 +151,6 @@ export const Network = React.memo<{ {sourceBytesPercent != null ? ( - - {`(${numeral(sourceBytesPercent).format('0.00')}%)`} - + {`(${numeral(sourceBytesPercent).format('0.00')}%)`} ) : null} - + @@ -95,7 +93,7 @@ const SourceArrow = React.memo<{ ) : null} - + {sourcePackets != null && !isNaN(Number(sourcePackets)) ? ( @@ -107,7 +105,7 @@ const SourceArrow = React.memo<{ value={sourcePackets} > - {`${sourcePackets} ${i18n.PACKETS}`} + {`${sourcePackets} ${i18n.PACKETS}`} @@ -171,11 +169,9 @@ const DestinationArrow = React.memo<{ > {destinationBytesPercent != null ? ( - - {`(${numeral(destinationBytesPercent).format('0.00')}%)`} - + {`(${numeral(destinationBytesPercent).format('0.00')}%)`} ) : null} - + @@ -196,9 +192,7 @@ const DestinationArrow = React.memo<{ value={destinationPackets} > - {`${numeral(destinationPackets).format( - '0,0' - )} ${i18n.PACKETS}`} + {`${numeral(destinationPackets).format('0,0')} ${i18n.PACKETS}`} @@ -264,7 +258,6 @@ export const SourceDestinationArrows = React.memo<{ return ( { - const mount = useMountAppended(); - describe('#isIpFieldPopulated', () => { test('it returns true when type is `source` and sourceIp has an IP address', () => { expect( @@ -333,7 +330,7 @@ describe('SourceDestinationIp', () => { test('it renders a `Source` label when type is `source` and (just) the sourceIp field is populated', () => { const type = 'source'; - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="source-label"]').first().text()).toEqual(i18n.SOURCE); + expect(screen.getByText(i18n.SOURCE)).toBeInTheDocument(); }); test('it renders a `Destination` label when type is `destination` and (just) the destinationIp field is populated', () => { const type = 'destination'; - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="destination-label"]').first().text()).toEqual( - i18n.DESTINATION - ); + expect(screen.getByText(i18n.DESTINATION)).toBeInTheDocument(); }); test('it renders a `Source` label when type is `source` (just) the sourcePort field is populated', () => { const type = 'source'; - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="source-label"]').first().text()).toEqual(i18n.SOURCE); + expect(screen.getByText(i18n.SOURCE)).toBeInTheDocument(); }); test('it renders a `Destination` label when type is `destination` and (just) the destinationPort field is populated', () => { const type = 'destination'; - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="destination-label"]').first().text()).toEqual( - i18n.DESTINATION - ); + expect(screen.getByText(i18n.DESTINATION)).toBeInTheDocument(); }); test('it renders a `Source` label when type is `source` and both sourceIp and sourcePort are populated', () => { const type = 'source'; - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="source-label"]').first().text()).toEqual(i18n.SOURCE); + expect(screen.getByText(i18n.SOURCE)).toBeInTheDocument(); }); test('it renders a `Destination` label when type is `destination` and both destinationIp and destinationPort are populated', () => { const type = 'destination'; - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="destination-label"]').first().text()).toEqual( - i18n.DESTINATION - ); + expect(screen.getByText(i18n.DESTINATION)).toBeInTheDocument(); }); test('it does NOT render a `Source` label when type is `source` and both sourceIp and sourcePort are empty', () => { const type = 'source'; - const wrapper = mount( + render( { ); - expect(wrapper.exists('[data-test-subj="source-label"]')).toBe(false); + expect(screen.queryByText(i18n.SOURCE)).not.toBeInTheDocument(); }); test('it does NOT render a `Destination` label when type is `destination` and both destinationIp and destinationPort are empty', () => { const type = 'destination'; - const wrapper = mount( + render( { ); - expect(wrapper.exists('[data-test-subj="destination-label"]')).toBe(false); + expect(screen.queryByText(i18n.DESTINATION)).not.toBeInTheDocument(); }); test('it renders the expected source IP when type is `source`, and both sourceIp and sourcePort are populated', () => { const type = 'source'; - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="draggable-truncatable-content"]').first().text()).toEqual( - '192.168.1.2' - ); + expect(screen.getByText('192.168.1.2')).toBeInTheDocument(); }); test('it renders the expected source IP when type is `source`, but the length of the sourceIp and sourcePort arrays is different', () => { const type = 'source'; - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="draggable-truncatable-content"]').first().text()).toEqual( - '192.168.1.2' - ); + expect(screen.getByText('192.168.1.2')).toBeInTheDocument(); }); test('it renders the expected destination IP when type is `destination`, and both destinationIp and destinationPort are populated', () => { const type = 'destination'; - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="draggable-truncatable-content"]').first().text()).toEqual( - '10.1.2.3' - ); + expect(screen.getByText('10.1.2.3')).toBeInTheDocument(); }); test('it renders the expected destination IP when type is `destination`, but the length of the destinationIp and destinationPort port arrays is different', () => { const type = 'destination'; - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="draggable-truncatable-content"]').first().text()).toEqual( - '10.1.2.3' - ); + expect(screen.getByText('10.1.2.3')).toBeInTheDocument(); }); test('it renders the expected source port when type is `source`, and both sourceIp and sourcePort are populated', () => { const type = 'source'; - const wrapper = mount( + render( { /> ); - expect( - removeExternalLinkText( - wrapper.find('[data-test-subj="draggable-content-source.port"]').first().text() - ) - ).toContain('9987'); + + expect(screen.getByTestId('source-ip-badge').textContent).toContain('9987'); }); test('it renders the expected destination port when type is `destination`, and both destinationIp and destinationPort are populated', () => { const type = 'destination'; - const wrapper = mount( + render( { ); - expect( - removeExternalLinkText( - wrapper.find('[data-test-subj="draggable-content-destination.port"]').first().text() - ) - ).toContain('80'); + expect(screen.getByTestId('destination-ip-badge').textContent).toContain('80'); }); test('it renders the expected source port when type is `source`, but only sourcePort is populated', () => { const type = 'source'; - const wrapper = mount( + render( { ); - expect( - removeExternalLinkText( - wrapper.find('[data-test-subj="draggable-content-source.port"]').first().text() - ) - ).toContain('9987'); + expect(screen.getByTestId('source-ip-badge').textContent).toContain('9987'); }); test('it renders the expected destination port when type is `destination`, and only destinationPort is populated', () => { const type = 'destination'; - const wrapper = mount( + render( { ); - expect( - removeExternalLinkText( - wrapper.find('[data-test-subj="draggable-content-destination.port"]').first().text() - ) - ).toContain('80'); + expect(screen.getByTestId('destination-ip-badge').textContent).toContain('80'); }); test('it does NOT render the badge when type is `source`, but both sourceIp and sourcePort are undefined', () => { const type = 'source'; - const wrapper = mount( + render( { ); - expect(wrapper.exists(`[data-test-subj="${type}-ip-badge"]`)).toBe(false); + expect(screen.queryByTestId(`${type}-ip-badge`)).not.toBeInTheDocument(); }); test('it does NOT render the badge when type is `destination`, but both destinationIp and destinationPort are undefined', () => { const type = 'destination'; - const wrapper = mount( + render( { ); - expect(wrapper.exists(`[data-test-subj="${type}-ip-badge"]`)).toBe(false); + expect(screen.queryByTestId(`${type}-ip-badge`)).not.toBeInTheDocument(); }); test('it renders geo fields', () => { const type = 'source'; - const wrapper = mount( + render( { ); - expect( - wrapper.find('[data-test-subj="draggable-content-source.geo.continent_name"]').first().text() - ).toEqual('North America'); + expect(screen.getByTestId('draggable-content-source.geo.continent_name').textContent).toBe( + 'North America' + ); }); }); diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.tsx index 2be1c5f50406f..df563ef6d7b99 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.tsx @@ -195,7 +195,7 @@ export const SourceDestinationIp = React.memo( gutterSize="xs" > - + {isIpFieldPopulated({ destinationIp, sourceIp, type }) ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx index 8b3f0bfdb107a..74613f51054f4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx @@ -6,20 +6,18 @@ */ import React from 'react'; +import { render, screen } from '@testing-library/react'; -import { removeExternalLinkText } from '@kbn/securitysolution-io-ts-utils'; import { TestProviders } from '../../../common/mock'; import '../../../common/mock/match_media'; -import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { CertificateFingerprint } from '.'; jest.mock('../../../common/lib/kibana'); describe('CertificateFingerprint', () => { - const mount = useMountAppended(); test('renders the expected label', () => { - const wrapper = mount( + render( { /> ); - expect(wrapper.find('[data-test-subj="fingerprint-label"]').first().text()).toEqual( - 'client cert' - ); + expect(screen.getByText('client cert')).toBeInTheDocument(); }); test('renders the fingerprint as text', () => { - const wrapper = mount( + render( { /> ); - expect( - removeExternalLinkText( - wrapper.find('[data-test-subj="certificate-fingerprint-link"]').first().text() - ) - ).toContain('3f4c57934e089f02ae7511200aee2d7e7aabd272'); + expect(screen.getByText('3f4c57934e089f02ae7511200aee2d7e7aabd272')).toBeInTheDocument(); }); test('it renders a hyperlink to an external site to compare the fingerprint against a known set of signatures', () => { - const wrapper = mount( + render( { /> ); - - expect( - wrapper.find('[data-test-subj="certificate-fingerprint-link"]').first().props().href - ).toEqual( + expect(screen.getByText('3f4c57934e089f02ae7511200aee2d7e7aabd272')).toHaveAttribute( + 'href', 'https://sslbl.abuse.ch/ssl-certificates/sha1/3f4c57934e089f02ae7511200aee2d7e7aabd272' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.tsx index 3baf64c51c4cb..c910589ceaa5b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.tsx @@ -60,7 +60,7 @@ export const CertificateFingerprint = React.memo<{ isAggregatable={true} fieldType="keyword" > - + {certificateType === 'client' ? i18n.CLIENT_CERT : i18n.SERVER_CERT} diff --git a/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx index 41dc70316a1a1..14aa532a68917 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx @@ -6,21 +6,19 @@ */ import React from 'react'; +import { render, screen } from '@testing-library/react'; import '../../../common/mock/match_media'; import { TestProviders } from '../../../common/mock'; import { ONE_MILLISECOND_AS_NANOSECONDS } from '../formatted_duration/helpers'; -import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { Duration } from '.'; jest.mock('../../../common/lib/kibana'); describe('Duration', () => { - const mount = useMountAppended(); - test('it renders the expected formatted duration', () => { - const wrapper = mount( + render( { /> ); - expect(wrapper.find('[data-test-subj="formatted-duration"]').first().text()).toEqual('1ms'); + expect(screen.getByText('1ms')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.test.tsx index 645487e6a584d..4a92bf323f2eb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.test.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { mount } from 'enzyme'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import React from 'react'; import { mockBrowserFields } from '../../../common/containers/source/mock'; @@ -18,17 +19,13 @@ import { import { StatefulEditDataProvider } from '.'; -interface HasIsDisabled { - isDisabled: boolean; -} - describe('StatefulEditDataProvider', () => { const field = 'client.address'; const timelineId = 'test'; const value = 'test-host'; test('it renders the current field', () => { - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="field"]').first().text()).toEqual(field); + expect(screen.getByText(field)).toBeInTheDocument(); }); test('it renders the expected placeholder for the current field when field is empty', () => { - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="field"]').first().props().placeholder).toEqual( - 'Select a field' - ); + expect(screen.getByText(/Select a field/)).toBeInTheDocument(); }); test('it renders the "is" operator in a humanized format', () => { - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="operator"]').first().text()).toEqual('is'); + expect(screen.getByText('is')).toBeInTheDocument(); }); test('it renders the negated "is" operator in a humanized format when isExcluded is true', () => { - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="operator"]').first().text()).toEqual('is not'); + expect(screen.getByText('is not')).toBeInTheDocument(); }); test('it renders the "exists" operator in human-readable format', () => { - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="operator"]').first().text()).toEqual('exists'); + expect(screen.getByText('exists')).toBeInTheDocument(); }); test('it renders the negated "exists" operator in a humanized format when isExcluded is true', () => { - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="operator"]').first().text()).toEqual('does not exist'); + expect(screen.getByText('does not exist')).toBeInTheDocument(); }); test('it renders the current value when the operator is "is"', () => { - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="value"]').first().props().value).toEqual(value); + expect(screen.getByDisplayValue(value)).toBeInTheDocument(); }); test('it renders the current value when the type of value is an array', () => { const reallyAnArray = [value] as unknown as string; - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="value"]').first().props().value).toEqual(value); + expect(screen.getByDisplayValue(value)).toBeInTheDocument(); }); test('it does NOT render the current value when the operator is "is not" (isExcluded is true)', () => { - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="value"]').first().props().value).toEqual(value); + expect(screen.getByDisplayValue(value)).toBeInTheDocument(); }); test('it renders the expected placeholder when value is empty', () => { - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="value"]').first().props().placeholder).toEqual('value'); + expect(screen.getByPlaceholderText('value')).toBeInTheDocument(); }); test('it does NOT render value when the operator is "exists"', () => { - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="value"]').exists()).toBe(false); + expect(screen.queryByPlaceholderText('value')).not.toBeInTheDocument(); }); test('it does NOT render value when the operator is "not exists" (isExcluded is true)', () => { - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="value"]').exists()).toBe(false); + expect(screen.queryByPlaceholderText('value')).not.toBeInTheDocument(); }); test('it does NOT render value when is template field', () => { - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="value"]').exists()).toBe(false); + expect(screen.queryByPlaceholderText('value')).not.toBeInTheDocument(); }); test('it does NOT disable the save button when field is valid', () => { - const wrapper = mount( + render( { ); - const props = wrapper.find('[data-test-subj="save"]').first().props() as HasIsDisabled; - - expect(props.isDisabled).toBe(false); + expect(screen.getByTestId('save')).not.toBeDisabled(); }); test('it disables the save button when field is invalid because it is empty', () => { - const wrapper = mount( + render( { ); - const props = wrapper.find('[data-test-subj="save"]').first().props() as HasIsDisabled; - - expect(props.isDisabled).toBe(true); + expect(screen.getByTestId('save')).toBeDisabled(); }); test('it disables the save button when field is invalid because it is not contained in the browser fields', () => { - const wrapper = mount( + render( { ); - const props = wrapper.find('[data-test-subj="save"]').first().props() as HasIsDisabled; - - expect(props.isDisabled).toBe(true); + expect(screen.getByTestId('save')).toBeDisabled(); }); test('it invokes onDataProviderEdited with the expected values when the user clicks the save button', () => { const onDataProviderEdited = jest.fn(); - const wrapper = mount( + render( { ); - wrapper.find('[data-test-subj="save"]').first().simulate('click'); - - wrapper.update(); + userEvent.click(screen.getByTestId('save')); expect(onDataProviderEdited).toBeCalledWith({ andProviderId: undefined, diff --git a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx index dcf32cb585f9e..9aa0f85f5fadf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx @@ -235,7 +235,6 @@ export const StatefulEditDataProvider = React.memo( + .c0 .euiPopover__anchor { + width: 100%; +} + +.c1 > span.euiToolTipAnchor { + display: block; +} + +.c1 > span.euiToolTipAnchor.eui-textTruncate { + display: inline-block; +} +
- - - - - / - - - - -
+
+
+
+
+
+
+ + + Test Org + + +
+
+
+
+
+
+
+ / +
+
+
+
+
+
+
+ + + 12345 + + +
+
+
+
+
+
+ + `; exports[`Field Renderers #dateRenderer it renders correctly against snapshot 1`] = ` - - - + + + Feb 7, 2019 @ 17:19:41.636 + + `; exports[`Field Renderers #hostIdRenderer it renders correctly against snapshot 1`] = ` - + + .c0 .euiPopover__anchor { + width: 100%; +} + +.c1 > span.euiToolTipAnchor { + display: block; +} + +.c1 > span.euiToolTipAnchor.eui-textTruncate { + display: inline-block; +} + +
+
+
+
+
+ + + + raspberrypi + + + +
+
+
+
+
+
`; exports[`Field Renderers #hostNameRenderer it renders correctly against snapshot 1`] = ` - + + .c0 .euiPopover__anchor { + width: 100%; +} + +.c1 > span.euiToolTipAnchor { + display: block; +} + +.c1 > span.euiToolTipAnchor.eui-textTruncate { + display: inline-block; +} + +
+
+
+
+
+ + + + raspberrypi + + + +
+
+
+
+
+
`; exports[`Field Renderers #locationRenderer it renders correctly against snapshot 1`] = ` + + .c0 .euiPopover__anchor { + width: 100%; +} + +.c1 > span.euiToolTipAnchor { + display: block; +} + +.c1 > span.euiToolTipAnchor.eui-textTruncate { + display: inline-block; +} +
- - - - ,  - - - -
+
+
+
+
+
+
+ + + New York + + +
+
+
+
+
+
+ ,  +
+
+
+
+
+
+ + + New York + + +
+
+
+
+
+
+ +
`; -exports[`Field Renderers #reputationRenderer it renders correctly against snapshot 1`] = ` - - - -`; +exports[`Field Renderers #reputationRenderer it renders correctly against snapshot 1`] = ``; exports[`Field Renderers #whoisRenderer it renders correctly against snapshot 1`] = ` - - iana.org - + + + + iana.org + + External link + + + (opens in a new tab or window) + + + + `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx index 7186b009e05aa..e6c745564ed3b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { TestProviders } from '../../../common/mock'; import '../../../common/mock/match_media'; @@ -25,60 +26,58 @@ import { MoreContainer, } from './field_renderers'; import { mockData } from '../../../network/components/details/mock'; -import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { AutonomousSystem, FlowTarget } from '../../../../common/search_strategy'; import { HostEcs } from '../../../../common/ecs/host'; -import { DataProvider } from '../../../../common/types'; jest.mock('../../../common/lib/kibana'); - -jest.mock('@elastic/eui', () => { - const original = jest.requireActual('@elastic/eui'); +jest.mock('../../../common/lib/kibana/kibana_react', () => { return { - ...original, - EuiScreenReaderOnly: () => <>, + useKibana: () => ({ + services: { + application: { + getUrlForApp: (appId: string, options?: { path?: string; deepLinkId?: boolean }) => + `${appId}/${options?.deepLinkId ?? ''}${options?.path ?? ''}`, + }, + }, + }), }; }); describe('Field Renderers', () => { - const mount = useMountAppended(); - describe('#locationRenderer', () => { test('it renders correctly against snapshot', () => { - const wrapper = shallow( + const { asFragment } = render( locationRenderer(['source.geo.city_name', 'source.geo.region_name'], mockData.complete) ); - expect(wrapper).toMatchSnapshot(); + expect(asFragment()).toMatchSnapshot(); }); test('it renders emptyTagValue when no fields provided', () => { - const wrapper = mount( - {locationRenderer([], mockData.complete)} - ); - expect(wrapper.text()).toEqual(getEmptyValue()); + render({locationRenderer([], mockData.complete)}); + expect(screen.getByText(getEmptyValue())).toBeInTheDocument(); }); test('it renders emptyTagValue when invalid fields provided', () => { - const wrapper = mount( + render( {locationRenderer(['source.geo.my_house'], mockData.complete)} ); - expect(wrapper.text()).toEqual(getEmptyValue()); + expect(screen.getByText(getEmptyValue())).toBeInTheDocument(); }); }); describe('#dateRenderer', () => { test('it renders correctly against snapshot', () => { - const wrapper = shallow(dateRenderer(mockData.complete.source?.firstSeen)); + const { asFragment } = render(dateRenderer(mockData.complete.source?.firstSeen)); - expect(wrapper).toMatchSnapshot(); + expect(asFragment()).toMatchSnapshot(); }); test('it renders emptyTagValue when invalid field provided', () => { - const wrapper = mount({dateRenderer(null)}); - expect(wrapper.text()).toEqual(getEmptyValue()); + render({dateRenderer(null)}); + expect(screen.getByText(getEmptyValue())).toBeInTheDocument(); }); }); @@ -87,25 +86,25 @@ describe('Field Renderers', () => { const halfEmptyMock: AutonomousSystem = { organization: { name: 'Test Org' }, number: null }; test('it renders correctly against snapshot', () => { - const wrapper = shallow( + const { asFragment } = render( autonomousSystemRenderer(mockData.complete.source!.autonomousSystem!, FlowTarget.source) ); - expect(wrapper).toMatchSnapshot(); + expect(asFragment()).toMatchSnapshot(); }); test('it renders emptyTagValue when non-string field provided', () => { - const wrapper = mount( + render( {autonomousSystemRenderer(halfEmptyMock, FlowTarget.source)} ); - expect(wrapper.text()).toEqual(getEmptyValue()); + expect(screen.getByText(getEmptyValue())).toBeInTheDocument(); }); test('it renders emptyTagValue when invalid field provided', () => { - const wrapper = mount( + render( {autonomousSystemRenderer(emptyMock, FlowTarget.source)} ); - expect(wrapper.text()).toEqual(getEmptyValue()); + expect(screen.getByText(getEmptyValue())).toBeInTheDocument(); }); }); @@ -121,29 +120,26 @@ describe('Field Renderers', () => { ip: undefined, }; test('it renders correctly against snapshot', () => { - const wrapper = shallow(hostNameRenderer(mockData.complete.host, '10.10.10.10')); - - expect(wrapper).toMatchSnapshot(); + const { asFragment } = render( + {hostNameRenderer(mockData.complete.host, '10.10.10.10')} + ); + expect(asFragment()).toMatchSnapshot(); }); test('it renders emptyTagValue when non-matching IP is provided', () => { - const wrapper = mount( + render( {hostNameRenderer(mockData.complete.host, '10.10.10.11')} ); - expect(wrapper.text()).toEqual(getEmptyValue()); + expect(screen.getByText(getEmptyValue())).toBeInTheDocument(); }); test('it renders emptyTagValue when no host.id is provided', () => { - const wrapper = mount( - {hostNameRenderer(emptyIdHost, FlowTarget.source)} - ); - expect(wrapper.text()).toEqual(getEmptyValue()); + render({hostNameRenderer(emptyIdHost, FlowTarget.source)}); + expect(screen.getByText(getEmptyValue())).toBeInTheDocument(); }); test('it renders emptyTagValue when no host.ip is provided', () => { - const wrapper = mount( - {hostNameRenderer(emptyIpHost, FlowTarget.source)} - ); - expect(wrapper.text()).toEqual(getEmptyValue()); + render({hostNameRenderer(emptyIpHost, FlowTarget.source)}); + expect(screen.getByText(getEmptyValue())).toBeInTheDocument(); }); }); @@ -164,66 +160,64 @@ describe('Field Renderers', () => { ip: ['10.10.10.10'], }; test('it renders correctly against snapshot', () => { - const wrapper = shallow(hostNameRenderer(mockData.complete.host, '10.10.10.10')); + const { asFragment } = render( + {hostNameRenderer(mockData.complete.host, '10.10.10.10')} + ); - expect(wrapper).toMatchSnapshot(); + expect(asFragment()).toMatchSnapshot(); }); test('it renders emptyTagValue when non-matching IP is provided', () => { - const wrapper = mount( + render( {hostNameRenderer(mockData.complete.host, '10.10.10.11')} ); - expect(wrapper.text()).toEqual(getEmptyValue()); + expect(screen.getByText(getEmptyValue())).toBeInTheDocument(); }); test('it renders emptyTagValue when no host.id is provided', () => { - const wrapper = mount( - {hostNameRenderer(emptyIdHost, FlowTarget.source)} - ); - expect(wrapper.text()).toEqual(getEmptyValue()); + render({hostNameRenderer(emptyIdHost, FlowTarget.source)}); + expect(screen.getByText(getEmptyValue())).toBeInTheDocument(); }); test('it renders emptyTagValue when no host.ip is provided', () => { - const wrapper = mount( - {hostNameRenderer(emptyIpHost, FlowTarget.source)} - ); - expect(wrapper.text()).toEqual(getEmptyValue()); + render({hostNameRenderer(emptyIpHost, FlowTarget.source)}); + expect(screen.getByText(getEmptyValue())).toBeInTheDocument(); }); test('it renders emptyTagValue when no host.name is provided', () => { - const wrapper = mount( - {hostNameRenderer(emptyNameHost, FlowTarget.source)} - ); - expect(wrapper.text()).toEqual(getEmptyValue()); + render({hostNameRenderer(emptyNameHost, FlowTarget.source)}); + expect(screen.getByText(getEmptyValue())).toBeInTheDocument(); }); }); describe('#whoisRenderer', () => { test('it renders correctly against snapshot', () => { - const wrapper = shallow(whoisRenderer('10.10.10.10')); + const { asFragment } = render(whoisRenderer('10.10.10.10')); - expect(wrapper).toMatchSnapshot(); + expect(asFragment()).toMatchSnapshot(); }); }); describe('#reputationRenderer', () => { test('it renders correctly against snapshot', () => { - const wrapper = shallow({reputationRenderer('10.10.10.10')}); + const { asFragment } = render( + {reputationRenderer('10.10.10.10')} + ); - expect(wrapper.find('DragDropContext')).toMatchSnapshot(); + expect(asFragment()).toMatchSnapshot(); }); }); describe('DefaultFieldRenderer', () => { test('it should render a single item', () => { - const wrapper = mount( + render( ); - expect(wrapper.text()).toEqual('item1 '); + expect(screen.getByTestId('DefaultFieldRendererComponent').textContent).toEqual('item1 '); }); test('it should render two items', () => { - const wrapper = mount( + render( { /> ); - expect(wrapper.text()).toEqual('item1,item2 '); + + expect(screen.getByTestId('DefaultFieldRendererComponent').textContent).toEqual( + 'item1,item2 ' + ); }); test('it should render all items when the item count exactly equals displayCount', () => { - const wrapper = mount( + render( { ); - expect(wrapper.text()).toEqual('item1,item2,item3,item4,item5 '); + expect(screen.getByTestId('DefaultFieldRendererComponent').textContent).toEqual( + 'item1,item2,item3,item4,item5 ' + ); }); test('it should render all items up to displayCount and the expected "+ n More" popover anchor text for items greater than displayCount', () => { - const wrapper = mount( + render( { /> ); - - expect(wrapper.text()).toEqual('item1,item2,item3,item4,item5 ,+2 More'); + expect(screen.getByTestId('DefaultFieldRendererComponent').textContent).toEqual( + 'item1,item2,item3,item4,item5 ,+2 More' + ); }); }); @@ -272,7 +272,7 @@ describe('Field Renderers', () => { const rowItems = ['item1', 'item2', 'item3', 'item4', 'item5', 'item6', 'item7']; test('it should only render the items after overflowIndexStart', () => { - const wrapper = mount( + render( { /> ); - expect(wrapper.text()).toEqual('item6item7'); + expect(screen.getByTestId('more-container').textContent).toEqual('item6item7'); }); test('it should render all the items when overflowIndexStart is zero', () => { - const wrapper = mount( + render( { /> ); - expect(wrapper.text()).toEqual('item1item2item3item4item5item6item7'); + expect(screen.getByTestId('more-container').textContent).toEqual( + 'item1item2item3item4item5item6item7' + ); }); test('it should have the eui-yScroll to enable scrolling when necessary', () => { - const wrapper = mount( + render( { /> ); - expect(wrapper.find('[data-test-subj="more-container"]').first().props().className).toEqual( - 'eui-yScroll' - ); + expect(screen.getByTestId('more-container')).toHaveClass('eui-yScroll'); }); test('it should use the moreMaxHeight prop as the value for the max-height style', () => { - const wrapper = mount( + render( { /> ); - expect( - wrapper.find('[data-test-subj="more-container"]').first().props().style?.maxHeight - ).toEqual(DEFAULT_MORE_MAX_HEIGHT); - }); - - test('it should render with correct attrName prop', () => { - const wrapper = mount( - - ); - - expect( - wrapper.find('DraggableWrapper').first().prop('dataProvider').queryMatch.field - ).toEqual('mock.attr'); - }); - - test('it should render with correct fieldType prop', () => { - const wrapper = mount( - + expect(screen.getByTestId('more-container')).toHaveStyle( + `max-height: ${DEFAULT_MORE_MAX_HEIGHT}` ); - - expect(wrapper.find('DraggableWrapper').first().prop('fieldType')).toEqual('keyword'); }); - test('it should render with correct isAggregatable prop', () => { - const wrapper = mount( + test('it should render with correct attrName prop', () => { + render( { /> ); - expect(wrapper.find('DraggableWrapper').first().prop('isAggregatable')).toEqual( - true - ); + screen + .getAllByTestId('render-content-mock.attr') + .forEach((element) => expect(element).toBeInTheDocument()); }); test('it should only invoke the optional render function, when provided, for the items after overflowIndexStart', () => { - const render = jest.fn(); + const renderFn = jest.fn(); - mount( + render( ); - expect(render).toHaveBeenCalledTimes(2); + expect(renderFn).toHaveBeenCalledTimes(2); }); }); @@ -411,7 +377,7 @@ describe('Field Renderers', () => { const rowItems = ['item1', 'item2', 'item3', 'item4', 'item5', 'item6', 'item7']; test('it should render the length of items after the overflowIndexStart', () => { - const wrapper = mount( + render( { ); - expect(wrapper.text()).toEqual(' ,+2 More'); - expect(wrapper.find('[data-test-subj="more-container"]').first().exists()).toBe(false); + expect(screen.getByTestId('DefaultFieldRendererOverflow-button').textContent).toEqual( + '+2 More' + ); + expect(screen.queryByTestId('more-container')).not.toBeInTheDocument(); }); test('it should render the items after overflowIndexStart in the popover', () => { - const wrapper = mount( + render( { ); - wrapper.find('button').first().simulate('click'); - wrapper.update(); - expect(wrapper.find('.euiPopover').first().exists()).toBe(true); - expect(wrapper.find('[data-test-subj="more-container"]').first().text()).toEqual( - 'item6item7' - ); + userEvent.click(screen.getByTestId('DefaultFieldRendererOverflow-button')); + + expect( + screen.getByText('You are in a dialog. To close this dialog, hit escape.') + ).toBeInTheDocument(); + expect(screen.getByTestId('more-container').textContent).toEqual('item6item7'); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx index 330e060e915c6..662a012033887 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx @@ -44,7 +44,7 @@ export const locationRenderer = ( isDraggable?: boolean ): React.ReactElement => fieldNames.length > 0 && fieldNames.every((fieldName) => getOr(null, fieldName, data)) ? ( - + {fieldNames.map((fieldName, index) => { const locationValue = getOr('', fieldName, data); return ( @@ -236,7 +236,12 @@ export const DefaultFieldRendererComponent: React.FC }); return draggables.length > 0 ? ( - + {draggables} ( <> {' ,'} - + {`+${rowItems.length - overflowIndexStart} `} + +
+
+
+
+
+
+
+ .c8 { + -webkit-transition: background-color 0.7s ease; + transition: background-color 0.7s ease; + width: 100%; + height: 100%; +} + +.c8 .flyout-overlay .euiPanel { + background-color: #16171c; +} + +.c8 > div.timeline-drop-area .drop-and-provider-timeline { + display: none; +} + +.c8 > div.timeline-drop-area + div { + display: none !important; +} + +.c14 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border-radius: 100%; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + font-size: 9px; + height: 34px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + margin: 0 5px 0 5px; + padding: 7px 6px 4px 6px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + width: 34px; +} + +.c14 .euiBadge__content { + position: relative; + top: -1px; +} + +.c14 .euiBadge__text { + text-overflow: clip; +} + +.c11 { + overflow: hidden; + margin: 5px 0 5px 0; + padding: 3px; + white-space: nowrap; +} + +.c13 { + height: 20px; + margin: 0 5px 0 5px; + maxwidth: 85px; + minwidth: 85px; +} + +.c12 { + background-color: #343741; +} + +.c9 { + width: auto; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-align-content: center; + -ms-flex-line-pack: center; + align-content: center; + min-height: 100px; +} + +.c9 + div { + display: none !important; +} + +.c10 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-flex-wrap: no-wrap; + -ms-flex-wrap: no-wrap; + flex-wrap: no-wrap; +} + +.c6 { + padding: 2px 0 4px 0; +} + +.is-dragging .c6 .drop-target-data-providers { + background: rgba(125,222,216,0.1); + border: 0.2rem dashed #7dded8; +} + +.is-dragging .c6 .drop-target-data-providers .timeline-drop-area-empty__text { + color: #7dded8; +} + +.is-dragging .c6 .drop-target-data-providers .euiFormHelpText { + color: #7dded8; +} + +.c7 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + padding-bottom: 2px; + position: relative; + border: 0.2rem dashed #535966; + border-radius: 5px; + padding: 4px 0; + margin: 2px 0 2px 0; + max-height: 33vh; + min-height: 100px; + overflow: auto; + resize: vertical; + background-color: #16171c; +} + +.c3 { + display: block; +} + +.c2 > span { + padding: 0; +} + +.c4 { + overflow: hidden; + display: inline-block; + text-overflow: ellipsis; +} + +.c0 { + margin: 0 -1px 0; +} + +.c1 { + overflow: hidden; +} + +.c5 { + border-radius: 0; + padding: 0 4px 0 4px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + z-index: 9000; +} + +
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ Drop anything +
+ + + + + highlighted + + + + +
+ here to build an +
+ + + + OR + + + +
+ query +
+
+
+
+ +
+
+
+
+
+
+
+
+ `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.test.tsx index 02381f8347ff2..e6855f52216ce 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_timeline_button/index.test.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; -import { waitFor } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { AddTimelineButton } from '.'; import { useKibana } from '../../../../common/lib/kibana'; @@ -19,6 +19,7 @@ import { } from '../../../../common/mock'; import { getAllTimeline, useGetAllTimeline } from '../../../containers/all'; import { mockHistory, Router } from '../../../../common/mock/router'; +import * as i18n from '../../timeline/properties/translations'; jest.mock('../../open_timeline/use_timeline_status', () => { const originalModule = jest.requireActual('../../open_timeline/use_timeline_status'); @@ -50,20 +51,11 @@ jest.mock('../../../containers/all', () => { }); jest.mock('../../timeline/properties/new_template_timeline', () => ({ - NewTemplateTimeline: jest.fn(() =>
), + NewTemplateTimeline: jest.fn(() =>
{'Create new timeline template'}
), })); jest.mock('../../timeline/properties/helpers', () => ({ - Description: jest.fn().mockReturnValue(
), - ExistingCase: jest.fn().mockReturnValue(
), - NewCase: jest.fn().mockReturnValue(
), - NewTimeline: jest.fn().mockReturnValue(
), - NotesButton: jest.fn().mockReturnValue(
), -})); - -jest.mock('../../../../common/components/inspect', () => ({ - InspectButton: jest.fn().mockReturnValue(
), - InspectButtonContainer: jest.fn(({ children }) =>
{children}
), + NewTimeline: jest.fn().mockReturnValue(
{'Create new timeline'}
), })); jest.mock('../../../../common/containers/source', () => ({ @@ -71,7 +63,6 @@ jest.mock('../../../../common/containers/source', () => ({ })); describe('AddTimelineButton', () => { - let wrapper: ReactWrapper; const props = { timelineId: TimelineId.active, }; @@ -89,36 +80,30 @@ describe('AddTimelineButton', () => { }, }, }); - wrapper = mount(); + render(); }); afterEach(() => { (useKibana as jest.Mock).mockReset(); }); - test('it renders settings-plus-in-circle', () => { - expect(wrapper.find('[data-test-subj="settings-plus-in-circle"]').exists()).toBeTruthy(); + test('it renders the add new timeline btn', () => { + expect(screen.getByLabelText(i18n.ADD_TIMELINE)).toBeInTheDocument(); }); test('it renders create timeline btn', async () => { - wrapper.find('[data-test-subj="settings-plus-in-circle"]').last().simulate('click'); - await waitFor(() => - expect(wrapper.find('[data-test-subj="create-default-btn"]').exists()).toBeTruthy() - ); + userEvent.click(screen.getByLabelText(i18n.ADD_TIMELINE)); + expect(screen.getByText(i18n.NEW_TIMELINE)).toBeInTheDocument(); }); - test('it renders create timeline template btn', async () => { - wrapper.find('[data-test-subj="settings-plus-in-circle"]').last().simulate('click'); - await waitFor(() => - expect(wrapper.find('[data-test-subj="create-template-btn"]').exists()).toBeTruthy() - ); + test('it renders create timeline template btn', () => { + userEvent.click(screen.getByLabelText(i18n.ADD_TIMELINE)); + expect(screen.getByText(i18n.NEW_TEMPLATE_TIMELINE)).toBeInTheDocument(); }); - test('it renders Open timeline btn', async () => { - wrapper.find('[data-test-subj="settings-plus-in-circle"]').last().simulate('click'); - await waitFor(() => - expect(wrapper.find('[data-test-subj="open-timeline-button"]').exists()).toBeTruthy() - ); + test('it renders Open timeline btn', () => { + userEvent.click(screen.getByLabelText(i18n.ADD_TIMELINE)); + expect(screen.getByTestId('open-timeline-button')).toBeInTheDocument(); }); }); @@ -135,36 +120,30 @@ describe('AddTimelineButton', () => { }, }, }); - wrapper = mount(); + render(); }); afterEach(() => { (useKibana as jest.Mock).mockReset(); }); - test('it renders settings-plus-in-circle', () => { - expect(wrapper.find('[data-test-subj="settings-plus-in-circle"]').exists()).toBeTruthy(); + test('it renders the add new timeline btn', () => { + expect(screen.getByLabelText(i18n.ADD_TIMELINE)).toBeInTheDocument(); }); - test('it renders create timeline btn', async () => { - wrapper.find('[data-test-subj="settings-plus-in-circle"]').last().simulate('click'); - await waitFor(() => - expect(wrapper.find('[data-test-subj="create-default-btn"]').exists()).toBeTruthy() - ); + test('it renders create timeline btn', () => { + userEvent.click(screen.getByLabelText(i18n.ADD_TIMELINE)); + expect(screen.getByText(i18n.NEW_TIMELINE)).toBeInTheDocument(); }); - test('it renders create timeline template btn', async () => { - wrapper.find('[data-test-subj="settings-plus-in-circle"]').last().simulate('click'); - await waitFor(() => - expect(wrapper.find('[data-test-subj="create-template-btn"]').exists()).toBeTruthy() - ); + test('it renders create timeline template btn', () => { + userEvent.click(screen.getByLabelText(i18n.ADD_TIMELINE)); + expect(screen.getByText(i18n.NEW_TEMPLATE_TIMELINE)).toBeInTheDocument(); }); test('it renders Open timeline btn', async () => { - wrapper.find('[data-test-subj="settings-plus-in-circle"]').last().simulate('click'); - await waitFor(() => - expect(wrapper.find('[data-test-subj="open-timeline-button"]').exists()).toBeTruthy() - ); + userEvent.click(screen.getByLabelText(i18n.ADD_TIMELINE)); + expect(screen.getByTestId('open-timeline-button')).toBeInTheDocument(); }); }); @@ -191,7 +170,7 @@ describe('AddTimelineButton', () => { refetch: jest.fn(), }); - wrapper = mount( + render( @@ -204,29 +183,24 @@ describe('AddTimelineButton', () => { (useKibana as jest.Mock).mockReset(); }); - it('should render timelines table', async () => { - wrapper.find('[data-test-subj="settings-plus-in-circle"]').last().simulate('click'); - await waitFor(() => { - expect(wrapper.find('[data-test-subj="open-timeline-button"]').exists()).toBeTruthy(); - }); + it('should render timelines table', () => { + userEvent.click(screen.getByLabelText(i18n.ADD_TIMELINE)); + expect(screen.getByTestId('open-timeline-button')).toBeInTheDocument(); - wrapper.find('[data-test-subj="open-timeline-button"]').first().simulate('click'); - await waitFor(() => { - expect(wrapper.find('[data-test-subj="timelines-table"]').exists()).toBeTruthy(); - }); + userEvent.click(screen.getByTestId('open-timeline-button')); + expect(screen.getByTestId('timelines-table')).toBeInTheDocument(); }); - it('should render correct actions', async () => { - wrapper.find('[data-test-subj="settings-plus-in-circle"]').last().simulate('click'); - await waitFor(() => - expect(wrapper.find('[data-test-subj="open-timeline-button"]').exists()).toBeTruthy() - ); + it('should render correct actions', () => { + userEvent.click(screen.getByLabelText(i18n.ADD_TIMELINE)); + expect(screen.getByTestId('open-timeline-button')).toBeInTheDocument(); + + userEvent.click(screen.getByTestId('open-timeline-button')); - wrapper.find('[data-test-subj="open-timeline-button"]').first().simulate('click'); - await waitFor(() => { - expect(wrapper.find('[data-test-subj="open-duplicate"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="create-from-template"]').exists()).toBeFalsy(); + screen.queryAllByTestId('open-duplicate').forEach((element) => { + expect(element).toBeInTheDocument(); }); + expect(screen.queryByTestId('create-from-template')).not.toBeInTheDocument(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.test.tsx index 7fd00667f57b0..b4466a3640a90 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.test.tsx @@ -6,7 +6,8 @@ */ import React from 'react'; -import { mount } from 'enzyme'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { useKibana } from '../../../../common/lib/kibana'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; @@ -54,13 +55,13 @@ describe('AddToCaseButton', () => { } ); (useDeepEqualSelector as jest.Mock).mockReturnValue(mockTimelineModel); - const wrapper = mount( + render( ); - wrapper.find(`[data-test-subj="attach-timeline-case-button"]`).first().simulate('click'); - wrapper.find(`[data-test-subj="attach-timeline-existing-case"]`).first().simulate('click'); + userEvent.click(screen.getByTestId('attach-timeline-case-button')); + userEvent.click(screen.getByTestId('attach-timeline-existing-case')); expect(navigateToApp).toHaveBeenCalledWith('securitySolutionUI', { path: '/create', @@ -76,13 +77,13 @@ describe('AddToCaseButton', () => { return <>; }); (useDeepEqualSelector as jest.Mock).mockReturnValue(mockTimelineModel); - const wrapper = mount( + render( ); - wrapper.find(`[data-test-subj="attach-timeline-case-button"]`).first().simulate('click'); - wrapper.find(`[data-test-subj="attach-timeline-existing-case"]`).first().simulate('click'); + userEvent.click(screen.getByTestId('attach-timeline-case-button')); + userEvent.click(screen.getByTestId('attach-timeline-existing-case')); expect(navigateToApp).toHaveBeenCalledWith('securitySolutionUI', { path: '/case-id', diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.test.tsx index 3b641d82ae18b..366b8d36fc7d4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import { mount } from 'enzyme'; import React from 'react'; +import { render, screen } from '@testing-library/react'; import { TestProviders } from '../../../../common/mock/test_providers'; import { TimelineTabs } from '../../../../../common/types/timeline'; @@ -14,7 +14,7 @@ import { FlyoutBottomBar } from '.'; describe('FlyoutBottomBar', () => { test('it renders the expected bottom bar', () => { - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="flyoutBottomBar"]').exists()).toBeTruthy(); + expect(screen.getByTestId('flyoutBottomBar')).toBeInTheDocument(); }); test('it renders the data providers drop target area', () => { - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="dataProviders"]').exists()).toBe(true); + expect(screen.getByTestId('dataProviders')).toBeInTheDocument(); }); test('it renders the flyout header panel', () => { - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="timeline-flyout-header-panel"]').exists()).toBe(true); + expect(screen.getByTestId('timeline-flyout-header-panel')).toBeInTheDocument(); }); test('it hides the data providers drop target area', () => { - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="dataProviders"]').exists()).toBe(false); + expect(screen.queryByTestId('dataProviders')).not.toBeInTheDocument(); }); test('it hides the flyout header panel', () => { - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="timeline-flyout-header-panel"]').exists()).toBe(false); + expect(screen.queryByTestId('timeline-flyout-header-panel')).not.toBeInTheDocument(); }); test('it renders the data providers drop target area when showDataproviders=false and tab is not query', () => { - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="dataProviders"]').exists()).toBe(true); + expect(screen.getByTestId('dataProviders')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx index a76e5e652d4ff..8906bff912d68 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { render, screen } from '@testing-library/react'; import { useKibana, useGetUserCasesPermissions } from '../../../../common/lib/kibana'; import { TestProviders, mockIndexNames, mockIndexPattern } from '../../../../common/mock'; @@ -14,7 +15,6 @@ import { useTimelineKpis } from '../../../containers/kpis'; import { FlyoutHeader } from '.'; import { useSourcererDataView } from '../../../../common/containers/sourcerer'; import { mockBrowserFields, mockDocValueFields } from '../../../../common/containers/source/mock'; -import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { getEmptyValue } from '../../../../common/components/empty_value'; const mockUseSourcererDataView: jest.Mock = useSourcererDataView as jest.Mock; @@ -58,8 +58,6 @@ const defaultMocks = { selectedPatterns: mockIndexNames, }; describe('header', () => { - const mount = useMountAppended(); - beforeEach(() => { // Mocking these services is required for the header component to render. mockUseSourcererDataView.mockImplementation(() => defaultMocks); @@ -86,13 +84,13 @@ describe('header', () => { read: false, }); - const wrapper = mount( + render( ); - expect(wrapper.find('[data-test-subj="attach-timeline-case-button"]').exists()).toBeTruthy(); + expect(screen.getByTestId('attach-timeline-case-button')).toBeInTheDocument(); }); it('does not render the button when the user does not have write permissions', () => { @@ -101,13 +99,13 @@ describe('header', () => { read: false, }); - const wrapper = mount( + render( ); - expect(wrapper.find('[data-test-subj="attach-timeline-case-button"]').exists()).toBeFalsy(); + expect(screen.queryByTestId('attach-timeline-case-button')).not.toBeInTheDocument(); }); }); @@ -116,21 +114,17 @@ describe('header', () => { beforeEach(() => { mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineKpiResponse]); }); - it('renders the component, labels and values successfully', async () => { - const wrapper = mount( + it('renders the component, labels and values successfully', () => { + render( ); - expect(wrapper.find('[data-test-subj="siem-timeline-kpis"]').exists()).toEqual(true); + expect(screen.getByTestId('siem-timeline-kpis')).toBeInTheDocument(); // label - expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual( - expect.stringContaining('Processes') - ); + expect(screen.getByText('Processes')).toBeInTheDocument(); // value - expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual( - expect.stringContaining('1') - ); + expect(screen.getByTestId('siem-timeline-process-kpi').textContent).toContain('1'); }); }); @@ -139,14 +133,12 @@ describe('header', () => { mockUseTimelineKpis.mockReturnValue([true, mockUseTimelineKpiResponse]); }); it('renders a loading indicator for values', async () => { - const wrapper = mount( + render( ); - expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual( - expect.stringContaining('--') - ); + expect(screen.getAllByText('--')).not.toHaveLength(0); }); }); @@ -154,19 +146,14 @@ describe('header', () => { beforeEach(() => { mockUseTimelineKpis.mockReturnValue([false, null]); }); - it('renders labels and the default empty string', async () => { - const wrapper = mount( + it('renders labels and the default empty string', () => { + render( ); - - expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual( - expect.stringContaining('Processes') - ); - expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual( - expect.stringContaining(getEmptyValue()) - ); + expect(screen.getByText('Processes')).toBeInTheDocument(); + expect(screen.getAllByText(getEmptyValue())).not.toHaveLength(0); }); }); @@ -174,24 +161,16 @@ describe('header', () => { beforeEach(() => { mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineLargeKpiResponse]); }); - it('formats the numbers correctly', async () => { - const wrapper = mount( + it('formats the numbers correctly', () => { + render( ); - expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual( - expect.stringContaining('1k') - ); - expect(wrapper.find('[data-test-subj="siem-timeline-user-kpi"]').first().text()).toEqual( - expect.stringContaining('1m') - ); - expect( - wrapper.find('[data-test-subj="siem-timeline-source-ip-kpi"]').first().text() - ).toEqual(expect.stringContaining('1b')); - expect(wrapper.find('[data-test-subj="siem-timeline-host-kpi"]').first().text()).toEqual( - expect.stringContaining('999') - ); + expect(screen.getByText('1k', { selector: '.euiTitle' })).toBeInTheDocument(); + expect(screen.getByText('1m', { selector: '.euiTitle' })).toBeInTheDocument(); + expect(screen.getByText('1b', { selector: '.euiTitle' })).toBeInTheDocument(); + expect(screen.getByText('999', { selector: '.euiTitle' })).toBeInTheDocument(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx index 322a9eceb8d5c..43a837344215b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx @@ -5,20 +5,13 @@ * 2.0. */ -import { mount, shallow } from 'enzyme'; -import { set } from '@elastic/safer-lodash-set/fp'; import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import '../../../common/mock/react_beautiful_dnd'; -import { - mockGlobalState, - TestProviders, - SUB_PLUGINS_REDUCER, - kibanaObservable, - createSecuritySolutionStorageMock, -} from '../../../common/mock'; +import { TestProviders } from '../../../common/mock'; import { TimelineId } from '../../../../common/types/timeline'; -import { createStore, State } from '../../../common/store'; import * as timelineActions from '../../store/timeline/actions'; import { Flyout } from '.'; @@ -38,8 +31,6 @@ jest.mock('../timeline', () => ({ })); describe('Flyout', () => { - const state: State = mockGlobalState; - const { storage } = createSecuritySolutionStorageMock(); const props = { onAppLeave: jest.fn(), timelineId: TimelineId.test, @@ -51,54 +42,32 @@ describe('Flyout', () => { describe('rendering', () => { test('it renders correctly against snapshot', () => { - const wrapper = shallow( + const { asFragment } = render( ); - expect(wrapper.find('Flyout')).toMatchSnapshot(); + expect(asFragment()).toMatchSnapshot(); }); test('it renders the default flyout state as a bottom bar', () => { - const wrapper = mount( + render( ); - expect(wrapper.find('[data-test-subj="flyoutBottomBar"]').first().text()).toContain( - 'Untitled timeline' - ); - }); - - test('it does NOT render the fly out bottom bar when its state is set to flyout is true', () => { - const stateShowIsTrue = set('timeline.timelineById.test.show', true, state); - const storeShowIsTrue = createStore( - stateShowIsTrue, - SUB_PLUGINS_REDUCER, - kibanaObservable, - storage - ); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="flyout-button-not-ready-to-drop"]').exists()).toEqual( - false - ); + expect(screen.getByText('Untitled timeline')).toBeInTheDocument(); }); test('should call the onOpen when the mouse is clicked for rendering', () => { - const wrapper = mount( + render( ); - wrapper.find('[data-test-subj="flyoutOverlay"]').first().simulate('click'); + userEvent.click(screen.getByTestId('flyoutOverlay')); expect(mockDispatch).toBeCalledWith(timelineActions.showTimeline({ id: 'test', show: true })); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.test.tsx index 31a06f88874b4..e16a7e6de6170 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ import React from 'react'; -import { mount } from 'enzyme'; -import { waitFor } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { FormattedIp } from '.'; import { TestProviders } from '../../../common/mock'; @@ -76,13 +76,13 @@ describe('FormattedIp', () => { toggleExpandedDetail.mockClear(); }); test('should render ip address', () => { - const wrapper = mount( + render( ); - expect(wrapper.text()).toEqual(props.value); + expect(screen.getByText(props.value)).toBeInTheDocument(); }); test('should render DraggableWrapper if isDraggable is true', () => { @@ -90,37 +90,35 @@ describe('FormattedIp', () => { ...props, isDraggable: true, }; - const wrapper = mount( + render( ); - expect(wrapper.find('[data-test-subj="DraggableWrapper"]').exists()).toEqual(true); + expect(screen.getByTestId('DraggableWrapper')).toBeInTheDocument(); }); - test('if not enableIpDetailsFlyout, should go to network details page', async () => { - const wrapper = mount( + test('if not enableIpDetailsFlyout, should go to network details page', () => { + render( ); - wrapper.find('[data-test-subj="network-details"]').last().simulate('click'); - await waitFor(() => { - expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled(); - expect(toggleExpandedDetail).not.toHaveBeenCalled(); - }); + userEvent.click(screen.getByTestId('network-details')); + expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled(); + expect(toggleExpandedDetail).not.toHaveBeenCalled(); }); - test('if enableIpDetailsFlyout, should open NetworkDetailsSidePanel', async () => { + test('if enableIpDetailsFlyout, should open NetworkDetailsSidePanel', () => { const context = { enableHostDetailsFlyout: true, enableIpDetailsFlyout: true, timelineID: TimelineId.active, tabType: TimelineTabs.query, }; - const wrapper = mount( + render( @@ -128,17 +126,15 @@ describe('FormattedIp', () => { ); - wrapper.find('[data-test-subj="network-details"]').last().simulate('click'); - await waitFor(() => { - expect(timelineActions.toggleDetailPanel).toHaveBeenCalledWith({ - panelView: 'networkDetail', - params: { - flowTarget: 'source', - ip: props.value, - }, - tabType: context.tabType, - timelineId: context.timelineID, - }); + userEvent.click(screen.getByTestId('network-details')); + expect(timelineActions.toggleDetailPanel).toHaveBeenCalledWith({ + panelView: 'networkDetail', + params: { + flowTarget: 'source', + ip: props.value, + }, + tabType: context.tabType, + timelineId: context.timelineID, }); }); @@ -149,7 +145,7 @@ describe('FormattedIp', () => { timelineID: TimelineId.active, tabType: TimelineTabs.query, }; - const wrapper = mount( + render( @@ -157,15 +153,13 @@ describe('FormattedIp', () => { ); - wrapper.find('[data-test-subj="network-details"]').last().simulate('click'); - await waitFor(() => { - expect(toggleExpandedDetail).toHaveBeenCalledWith({ - panelView: 'networkDetail', - params: { - flowTarget: 'source', - ip: props.value, - }, - }); + userEvent.click(screen.getByTestId('network-details')); + expect(toggleExpandedDetail).toHaveBeenCalledWith({ + panelView: 'networkDetail', + params: { + flowTarget: 'source', + ip: props.value, + }, }); }); @@ -176,7 +170,7 @@ describe('FormattedIp', () => { timelineID: 'detection', tabType: TimelineTabs.query, }; - const wrapper = mount( + render( @@ -184,18 +178,16 @@ describe('FormattedIp', () => { ); - wrapper.find('[data-test-subj="network-details"]').last().simulate('click'); - await waitFor(() => { - expect(timelineActions.toggleDetailPanel).toHaveBeenCalledWith({ - panelView: 'networkDetail', - params: { - flowTarget: 'source', - ip: props.value, - }, - tabType: context.tabType, - timelineId: context.timelineID, - }); - expect(toggleExpandedDetail).not.toHaveBeenCalled(); + userEvent.click(screen.getByTestId('network-details')); + expect(timelineActions.toggleDetailPanel).toHaveBeenCalledWith({ + panelView: 'networkDetail', + params: { + flowTarget: 'source', + ip: props.value, + }, + tabType: context.tabType, + timelineId: context.timelineID, }); + expect(toggleExpandedDetail).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx index ddbba7f2bc9f3..61491dd62a254 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx @@ -6,21 +6,18 @@ */ import React from 'react'; +import { render, screen } from '@testing-library/react'; -import { removeExternalLinkText } from '@kbn/securitysolution-io-ts-utils'; import { TestProviders } from '../../../common/mock'; import '../../../common/mock/match_media'; -import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { Ja3Fingerprint } from '.'; jest.mock('../../../common/lib/kibana'); describe('Ja3Fingerprint', () => { - const mount = useMountAppended(); - test('renders the expected label', () => { - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="ja3-fingerprint-label"]').first().text()).toEqual('ja3'); + expect(screen.getByText('ja3')).toBeInTheDocument(); }); test('renders the fingerprint as text', () => { - const wrapper = mount( + render( { ); - expect( - removeExternalLinkText(wrapper.find('[data-test-subj="ja3-fingerprint-link"]').first().text()) - ).toContain('fff799d91b7c01ae3fe6787cfc895552'); + expect(screen.getByText('fff799d91b7c01ae3fe6787cfc895552')).toBeInTheDocument(); }); test('it renders a hyperlink to an external site to compare the fingerprint against a known set of signatures', () => { - const wrapper = mount( + render( { ); - expect(wrapper.find('[data-test-subj="ja3-fingerprint-link"]').first().props().href).toEqual( + expect(screen.getByText('fff799d91b7c01ae3fe6787cfc895552')).toHaveAttribute( + 'href', 'https://sslbl.abuse.ch/ja3-fingerprints/fff799d91b7c01ae3fe6787cfc895552' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.tsx index de566d3db5c18..785226f74b003 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.tsx @@ -44,9 +44,7 @@ export const Ja3Fingerprint = React.memo<{ isAggregatable={true} fieldType="keyword" > - - {i18n.JA3_FINGERPRINT_LABEL} - + {i18n.JA3_FINGERPRINT_LABEL} )); diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/netflow/__snapshots__/index.test.tsx.snap index 1a56de1c2c6a2..0ded968e8526b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/__snapshots__/index.test.tsx.snap @@ -1,189 +1,2757 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Netflow renders correctly against snapshot 1`] = ` - - - - - + .c13 svg { + position: relative; + top: -1px; +} + +.c11, +.c11 * { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: top; + white-space: nowrap; +} + +.c1 .euiPopover__anchor { + width: 100%; +} + +.c3 { + border-radius: 2px; + padding: 0 4px 0 8px; + position: relative; + z-index: 0 !important; +} + +.c3::before { + background-image: linear-gradient( 135deg, #535966 25%, transparent 25% ), linear-gradient( -135deg, #535966 25%, transparent 25% ), linear-gradient( 135deg, transparent 75%, #535966 75% ), linear-gradient( -135deg, transparent 75%, #535966 75% ); + background-position: 0 0,1px 0,1px -1px,0px 1px; + background-size: 2px 2px; + bottom: 2px; + content: ''; + display: block; + left: 2px; + position: absolute; + top: 2px; + width: 4px; +} + +.c3:hover, +.c3:hover .euiBadge, +.c3:hover .euiBadge__text { + cursor: move; + cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; +} + +.event-column-view:hover .c3::before, +tr:hover .c3::before { + background-image: linear-gradient( 135deg, #98a2b3 25%, transparent 25% ), linear-gradient( -135deg, #98a2b3 25%, transparent 25% ), linear-gradient( 135deg, transparent 75%, #98a2b3 75% ), linear-gradient( -135deg, transparent 75%, #98a2b3 75% ); +} + +.c3:hover, +.c3:focus, +.event-column-view:hover .c3:hover, +.event-column-view:focus .c3:focus, +tr:hover .c3:hover, +tr:hover .c3:focus { + background-color: #36a2ef; +} + +.c3:hover, +.c3:focus, +.event-column-view:hover .c3:hover, +.event-column-view:focus .c3:focus, +tr:hover .c3:hover, +tr:hover .c3:focus, +.c3:hover a, +.c3:focus a, +.event-column-view:hover .c3:hover a, +.event-column-view:focus .c3:focus a, +tr:hover .c3:hover a, +tr:hover .c3:focus a, +.c3:hover a:hover, +.c3:focus a:hover, +.event-column-view:hover .c3:hover a:hover, +.event-column-view:focus .c3:focus a:hover, +tr:hover .c3:hover a:hover, +tr:hover .c3:focus a:hover { + color: #1d1e24; +} + +.c3:hover::before, +.c3:focus::before, +.event-column-view:hover .c3:hover::before, +.event-column-view:focus .c3:focus::before, +tr:hover .c3:hover::before, +tr:hover .c3:focus::before { + background-image: linear-gradient( 135deg, #1d1e24 25%, transparent 25% ), linear-gradient( -135deg, #1d1e24 25%, transparent 25% ), linear-gradient( 135deg, transparent 75%, #1d1e24 75% ), linear-gradient( -135deg, transparent 75%, #1d1e24 75% ); +} + +.c21 { + margin-right: 5px; +} + +.c7 { + margin-right: 3px; +} + +.c8 { + margin: 0 5px; +} + +.c17 { + background-color: #343741; + height: 2.8px; + width: 25px; +} + +.c20 { + background-color: #343741; + height: 2.2px; + width: 25px; +} + +.c19 { + margin-right: 5px; +} + +.c16 { + margin: 0 2px; +} + +.c16 .euiToolTipAnchor { + white-space: nowrap; +} + +.c18 { + margin: 0 5px; +} + +.c15 { + position: relative; + top: 1px; +} + +.c14 { + margin-right: 5px; +} + +.c12 { + margin: 0 3px; +} + +.c10 { + font-weight: bold; + margin-top: 2px; +} + +.c9 { + margin-top: 3px; +} + +.c6 { + margin-right: 3px; + position: relative; + top: -1px; +} + +.c0 { + margin-right: 10px; +} + +.c2 { + display: inline-block; + max-width: 100%; +} + +.c2 [data-rbd-placeholder-context-id] { + display: none !important; +} + +.c4 > span.euiToolTipAnchor { + display: block; +} + +.c4 > span.euiToolTipAnchor.eui-textTruncate { + display: inline-block; +} + +.c5 { + vertical-align: top; +} + +.c22 { + margin-right: 5px; +} + +
- - - +
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + first.last + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + rat + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + +
+ 1ms +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + Nov 12, 2018 @ 19:03:25.836 + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + Nov 12, 2018 @ 19:03:25.936 + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + outgoing + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + http + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+ + 100B + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+ + 3 pkts + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + tcp + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + we.live.in.a + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+ Source +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + 192.168.1.2 + + + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + North America + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + United States + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + 🇺🇸 + +
+
+
+
+
+
+
+
+
+
+ + + US + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + Georgia + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + Atlanta + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ + +
+ + (60.00%) + + + 60B + +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ + +
+ + 2 pkts + +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+ + +
+ + (40.00%) + + + 40B + +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ + +
+ + 1 pkts + +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+ + + +
+
+
+ Destination +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + 10.1.2.3 + + + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + North America + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + United States + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + 🇺🇸 + +
+
+
+
+
+
+
+
+
+
+ + + US + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + New York + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + New York + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx index 9ccabf2f47d44..48dd8c9ddfd87 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx @@ -7,9 +7,8 @@ import { get } from 'lodash/fp'; import React from 'react'; -import { shallow } from 'enzyme'; +import { render, screen, within } from '@testing-library/react'; -import { removeExternalLinkText } from '@kbn/securitysolution-io-ts-utils'; import { asArrayIfExists } from '../../../common/lib/helpers'; import '../../../common/mock/match_media'; import { TestProviders } from '../../../common/mock/test_providers'; @@ -59,7 +58,6 @@ import { NETWORK_PROTOCOL_FIELD_NAME, NETWORK_TRANSPORT_FIELD_NAME, } from '../../../network/components/source_destination/field_names'; -import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { getMockNetflowData } from '../../../common/mock/netflow'; jest.mock('../../../common/lib/kibana'); @@ -136,300 +134,244 @@ const getNetflowInstance = () => ( jest.mock('../../../common/components/link_to'); describe('Netflow', () => { - const mount = useMountAppended(); - test('renders correctly against snapshot', () => { - const wrapper = shallow(getNetflowInstance()); - expect(wrapper).toMatchSnapshot(); + const { asFragment } = render({getNetflowInstance()}); + expect(asFragment()).toMatchSnapshot(); }); test('it renders a destination label', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="destination-label"]').first().text()).toEqual( - i18n.DESTINATION - ); + expect(screen.getByText(i18n.DESTINATION)).toBeInTheDocument(); }); test('it renders destination.bytes', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="destination-bytes"]').first().text()).toEqual('40B'); + expect(screen.getByText('40B')).toBeInTheDocument(); }); test('it renders destination.geo.continent_name', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect( - wrapper.find('[data-test-subj="destination.geo.continent_name"]').first().text() - ).toEqual('North America'); + expect(screen.getByTestId('draggable-content-destination.geo.continent_name').textContent).toBe( + 'North America' + ); }); test('it renders destination.geo.country_name', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="destination.geo.country_name"]').first().text()).toEqual( + expect(screen.getByTestId('draggable-content-destination.geo.country_name').textContent).toBe( 'United States' ); }); test('it renders destination.geo.country_iso_code', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); expect( - wrapper.find('[data-test-subj="destination.geo.country_iso_code"]').first().text() - ).toEqual('US'); + screen.getByTestId('draggable-content-destination.geo.country_iso_code').textContent + ).toBe('US'); }); test('it renders destination.geo.region_name', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="destination.geo.region_name"]').first().text()).toEqual( + expect(screen.getByTestId('draggable-content-destination.geo.region_name').textContent).toBe( 'New York' ); }); test('it renders destination.geo.city_name', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="destination.geo.city_name"]').first().text()).toEqual( + expect(screen.getByTestId('draggable-content-destination.geo.city_name').textContent).toBe( 'New York' ); }); test('it renders the destination ip and port, separated with a colon', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect( - removeExternalLinkText( - wrapper.find('[data-test-subj="destination-ip-and-port"]').first().text() - ) - ).toContain('10.1.2.3:80'); + expect(screen.getByTestId('destination-ip-badge').textContent).toContain('10.1.2.3:80'); }); test('it renders destination.packets', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="destination-packets"]').first().text()).toEqual('1 pkts'); + expect(screen.getByText('1 pkts')).toBeInTheDocument(); }); test('it hyperlinks links destination.port to an external service that describes the purpose of the port', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); expect( - wrapper - .find('[data-test-subj="destination-ip-and-port"]') - .find('[data-test-subj="port-or-service-name-link"]') - .first() - .props().href - ).toEqual( + within(screen.getByTestId('destination-ip-group')).getByTestId('port-or-service-name-link') + ).toHaveAttribute( + 'href', 'https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=80' ); }); test('it renders event.duration', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="event-duration"]').first().text()).toEqual('1ms'); + expect(screen.getByText('1ms')).toBeInTheDocument(); }); test('it renders event.end', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="event-end"]').first().text().length).toBeGreaterThan(0); // the format of this date will depend on the user's locale and settings + expect(screen.getByTestId('draggable-content-event.end').textContent).toBe( + 'Nov 12, 2018 @ 19:03:25.936' + ); }); test('it renders event.start', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="event-start"]').first().text().length).toBeGreaterThan(0); // the format of this date will depend on the user's locale and settings + expect(screen.getByTestId('draggable-content-event.start').textContent).toBe( + 'Nov 12, 2018 @ 19:03:25.836' + ); }); test('it renders network.bytes', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="network-bytes"]').first().text()).toEqual('100B'); + expect(screen.getByText('100B')).toBeInTheDocument(); }); test('it renders network.community_id', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="network-community-id"]').first().text()).toEqual( - 'we.live.in.a' - ); + expect(screen.getByText('we.live.in.a')).toBeInTheDocument(); }); test('it renders network.direction', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="network-direction"]').first().text()).toEqual('outgoing'); + expect(screen.getByText('outgoing')).toBeInTheDocument(); }); test('it renders network.packets', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="network-packets"]').first().text()).toEqual('3 pkts'); + expect(screen.getByText('3 pkts')).toBeInTheDocument(); }); test('it renders network.protocol', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="network-protocol"]').first().text()).toEqual('http'); + expect(screen.getByText('http')).toBeInTheDocument(); }); test('it renders process.name', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="process-name"]').first().text()).toEqual('rat'); + expect(screen.getByText('rat')).toBeInTheDocument(); }); test('it renders a source label', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="source-label"]').first().text()).toEqual(i18n.SOURCE); + expect(screen.getByText(i18n.SOURCE)).toBeInTheDocument(); }); test('it renders source.bytes', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="source-bytes"]').first().text()).toEqual('60B'); + expect(screen.getByText('60B')).toBeInTheDocument(); }); test('it renders source.geo.continent_name', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="source.geo.continent_name"]').first().text()).toEqual( + expect(screen.getByTestId('draggable-content-source.geo.continent_name').textContent).toBe( 'North America' ); }); test('it renders source.geo.country_name', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="source.geo.country_name"]').first().text()).toEqual( + expect(screen.getByTestId('draggable-content-source.geo.country_name').textContent).toBe( 'United States' ); }); test('it renders source.geo.country_iso_code', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="source.geo.country_iso_code"]').first().text()).toEqual( + expect(screen.getByTestId('draggable-content-source.geo.country_iso_code').textContent).toBe( 'US' ); }); test('it renders source.geo.region_name', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="source.geo.region_name"]').first().text()).toEqual( + expect(screen.getByTestId('draggable-content-source.geo.region_name').textContent).toBe( 'Georgia' ); }); test('it renders source.geo.city_name', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="source.geo.city_name"]').first().text()).toEqual( + expect(screen.getByTestId('draggable-content-source.geo.city_name').textContent).toBe( 'Atlanta' ); }); test('it renders the source ip and port, separated with a colon', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect( - removeExternalLinkText(wrapper.find('[data-test-subj="source-ip-and-port"]').first().text()) - ).toContain('192.168.1.2:9987'); + expect(screen.getByTestId('source-ip-badge').textContent).toContain('192.168.1.2:9987'); }); test('it renders source.packets', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="source-packets"]').first().text()).toEqual('2 pkts'); + expect(screen.getByText('2 pkts')).toBeInTheDocument(); }); test('it hyperlinks tls.client_certificate.fingerprint.sha1 site to compare the fingerprint against a known set of signatures', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect( - wrapper - .find('[data-test-subj="client-certificate-fingerprint"]') - .find('[data-test-subj="certificate-fingerprint-link"]') - .first() - .props().href - ).toEqual( + expect(screen.getByText('tls.client_certificate.fingerprint.sha1-value')).toHaveAttribute( + 'href', 'https://sslbl.abuse.ch/ssl-certificates/sha1/tls.client_certificate.fingerprint.sha1-value' ); }); - test('renders tls.client_certificate.fingerprint.sha1 text', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - removeExternalLinkText( - wrapper - .find('[data-test-subj="client-certificate-fingerprint"]') - .find('[data-test-subj="certificate-fingerprint-link"]') - .first() - .text() - ) - ).toContain('tls.client_certificate.fingerprint.sha1-value'); - }); - test('it hyperlinks tls.fingerprints.ja3.hash site to compare the fingerprint against a known set of signatures', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="ja3-fingerprint-link"]').first().props().href).toEqual( + expect(screen.getByText('tls.fingerprints.ja3.hash-value')).toHaveAttribute( + 'href', 'https://sslbl.abuse.ch/ja3-fingerprints/tls.fingerprints.ja3.hash-value' ); }); - test('renders tls.fingerprints.ja3.hash text', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - removeExternalLinkText(wrapper.find('[data-test-subj="ja3-fingerprint-link"]').first().text()) - ).toContain('tls.fingerprints.ja3.hash-value'); - }); - test('it hyperlinks tls.server_certificate.fingerprint.sha1 site to compare the fingerprint against a known set of signatures', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect( - wrapper - .find('[data-test-subj="server-certificate-fingerprint"]') - .find('[data-test-subj="certificate-fingerprint-link"]') - .first() - .props().href - ).toEqual( + expect(screen.getByText('tls.server_certificate.fingerprint.sha1-value')).toHaveAttribute( + 'href', 'https://sslbl.abuse.ch/ssl-certificates/sha1/tls.server_certificate.fingerprint.sha1-value' ); }); - test('renders tls.server_certificate.fingerprint.sha1 text', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - removeExternalLinkText( - wrapper - .find('[data-test-subj="server-certificate-fingerprint"]') - .find('[data-test-subj="certificate-fingerprint-link"]') - .first() - .text() - ) - ).toContain('tls.server_certificate.fingerprint.sha1-value'); - }); - test('it renders network.transport', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="network-transport"]').first().text()).toEqual('tcp'); + expect(screen.getByText('tcp')).toBeInTheDocument(); }); test('it renders user.name', () => { - const wrapper = mount({getNetflowInstance()}); + render({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="user-name"]').first().text()).toEqual('first.last'); + expect(screen.getByText('first.last')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/duration_event_start_end.tsx b/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/duration_event_start_end.tsx index cf479ae8f8f0e..986ca8bb75e2b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/duration_event_start_end.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/netflow_columns/duration_event_start_end.tsx @@ -51,7 +51,6 @@ export const DurationEventStartEnd = React.memo<{ ? uniq(eventDuration).map((duration) => ( ( ( - -
- -
-
- + + .c15 svg { + position: relative; + top: -1px; +} + +.c0 { + font-size: 12px; + line-height: 1.5; + padding-left: 12px; +} + +.c0 .euiAccordion + div { + background-color: #1d1e24; + padding: 0 8px; + border: 1px solid #343741; + border-radius: 4px; +} + +.c13, +.c13 * { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: top; + white-space: nowrap; +} + +.c3 .euiPopover__anchor { + width: 100%; +} + +.c5 { + border-radius: 2px; + padding: 0 4px 0 8px; + position: relative; + z-index: 0 !important; +} + +.c5::before { + background-image: linear-gradient( 135deg, #535966 25%, transparent 25% ), linear-gradient( -135deg, #535966 25%, transparent 25% ), linear-gradient( 135deg, transparent 75%, #535966 75% ), linear-gradient( -135deg, transparent 75%, #535966 75% ); + background-position: 0 0,1px 0,1px -1px,0px 1px; + background-size: 2px 2px; + bottom: 2px; + content: ''; + display: block; + left: 2px; + position: absolute; + top: 2px; + width: 4px; +} + +.c5:hover, +.c5:hover .euiBadge, +.c5:hover .euiBadge__text { + cursor: move; + cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; +} + +.event-column-view:hover .c5::before, +tr:hover .c5::before { + background-image: linear-gradient( 135deg, #98a2b3 25%, transparent 25% ), linear-gradient( -135deg, #98a2b3 25%, transparent 25% ), linear-gradient( 135deg, transparent 75%, #98a2b3 75% ), linear-gradient( -135deg, transparent 75%, #98a2b3 75% ); +} + +.c5:hover, +.c5:focus, +.event-column-view:hover .c5:hover, +.event-column-view:focus .c5:focus, +tr:hover .c5:hover, +tr:hover .c5:focus { + background-color: #36a2ef; +} + +.c5:hover, +.c5:focus, +.event-column-view:hover .c5:hover, +.event-column-view:focus .c5:focus, +tr:hover .c5:hover, +tr:hover .c5:focus, +.c5:hover a, +.c5:focus a, +.event-column-view:hover .c5:hover a, +.event-column-view:focus .c5:focus a, +tr:hover .c5:hover a, +tr:hover .c5:focus a, +.c5:hover a:hover, +.c5:focus a:hover, +.event-column-view:hover .c5:hover a:hover, +.event-column-view:focus .c5:focus a:hover, +tr:hover .c5:hover a:hover, +tr:hover .c5:focus a:hover { + color: #1d1e24; +} + +.c5:hover::before, +.c5:focus::before, +.event-column-view:hover .c5:hover::before, +.event-column-view:focus .c5:focus::before, +tr:hover .c5:hover::before, +tr:hover .c5:focus::before { + background-image: linear-gradient( 135deg, #1d1e24 25%, transparent 25% ), linear-gradient( -135deg, #1d1e24 25%, transparent 25% ), linear-gradient( 135deg, transparent 75%, #1d1e24 75% ), linear-gradient( -135deg, transparent 75%, #1d1e24 75% ); +} + +.c23 { + margin-right: 5px; +} + +.c9 { + margin-right: 3px; +} + +.c10 { + margin: 0 5px; +} + +.c19 { + background-color: #343741; + height: 2.8px; + width: 25px; +} + +.c22 { + background-color: #343741; + height: 2.2px; + width: 25px; +} + +.c21 { + margin-right: 5px; +} + +.c18 { + margin: 0 2px; +} + +.c18 .euiToolTipAnchor { + white-space: nowrap; +} + +.c20 { + margin: 0 5px; +} + +.c17 { + position: relative; + top: 1px; +} + +.c16 { + margin-right: 5px; +} + +.c14 { + margin: 0 3px; +} + +.c12 { + font-weight: bold; + margin-top: 2px; +} + +.c11 { + margin-top: 3px; +} + +.c8 { + margin-right: 3px; + position: relative; + top: -1px; +} + +.c2 { + margin-right: 10px; +} + +.c4 { + display: inline-block; + max-width: 100%; +} + +.c4 [data-rbd-placeholder-context-id] { + display: none !important; +} + +.c6 > span.euiToolTipAnchor { + display: block; +} + +.c6 > span.euiToolTipAnchor.eui-textTruncate { + display: inline-block; +} + +.c7 { + vertical-align: top; +} + +.c24 { + margin-right: 5px; +} + +.c1 { + margin: 5px 0; +} + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ user.name +

+ + + + + + first.last + + + + + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ process.name +

+ + + + + + rat + + + + + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ event.duration +

+ +
+ + +
+ 1ms +
+
+
+
+

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ event.start +

+ +
+ + + Nov 12, 2018 @ 19:03:25.836 + +
+
+

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ event.end +

+ +
+ + + Nov 12, 2018 @ 19:03:25.936 + +
+
+

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ network.direction +

+ + + + + + outgoing + + + + + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ network.protocol +

+ + + + + + http + + + + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ network.bytes +

+ + +
+ + 100B + +
+
+
+

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ network.packets +

+ + +
+ + 3 pkts + +
+
+
+

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ network.transport +

+ + + + + + tcp + + + + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ network.community_id +

+ + + + + + we.live.in.a + + + + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+ Source +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ source.ip +

+ + + + 192.168.1.2 + + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+ + : + +
+
+
+
+
+
+
+
+
+
+

+ source.port +

+ + + + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ source.geo.continent_name +

+ + + North America + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ source.geo.country_name +

+ + + United States + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + 🇺🇸 + +
+
+
+
+
+
+
+
+
+
+

+ source.geo.country_iso_code +

+ + + US + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ source.geo.region_name +

+ + + Georgia + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ source.geo.city_name +

+ + + Atlanta + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+

+ source.bytes +

+ + +
+ + (60.00%) + + + 60B + +
+
+
+

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+

+ source.packets +

+ + +
+ + 2 pkts + +
+
+
+

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+

+ destination.bytes +

+ + +
+ + (40.00%) + + + 40B + +
+
+
+

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+

+ destination.packets +

+ + +
+ + 1 pkts + +
+
+
+

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+ + + +
+
+
+ Destination +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ destination.ip +

+ + + + 10.1.2.3 + + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+ + : + +
+
+
+
+
+
+
+
+
+
+

+ destination.port +

+ + + + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ destination.geo.continent_name +

+ + + North America + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ destination.geo.country_name +

+ + + United States + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + 🇺🇸 + +
+
+
+
+
+
+
+
+
+
+

+ destination.geo.country_iso_code +

+ + + US + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ destination.geo.region_name +

+ + + New York + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ destination.geo.city_name +

+ + + New York + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ tls.fingerprints.ja3.hash +

+ + + + + + + ja3 + + + tls.fingerprints.ja3.hash-value + + External link + + + (opens in a new tab or window) + + + + + + + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ tls.client_certificate.fingerprint.sha1 +

+ + + + + + + client cert + + + tls.client_certificate.fingerprint.sha1-value + + External link + + + (opens in a new tab or window) + + + + + + + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ tls.server_certificate.fingerprint.sha1 +

+ + + + + + + server cert + + + tls.server_certificate.fingerprint.sha1-value + + External link + + + (opens in a new tab or window) + + + + + + + + +

+ Press enter for options, or press space to begin dragging. +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx index f2f95dec90144..36971879962e5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx @@ -5,13 +5,12 @@ * 2.0. */ -import { shallow } from 'enzyme'; import React from 'react'; +import { render, screen } from '@testing-library/react'; import '../../../../../../common/mock/match_media'; import { Ecs } from '../../../../../../../common/ecs'; import { getMockNetflowData, TestProviders } from '../../../../../../common/mock'; -import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { eventActionMatches, @@ -29,8 +28,6 @@ jest.mock('../../../../../../common/lib/kibana'); jest.mock('../../../../../../common/components/link_to'); describe('netflowRowRenderer', () => { - const mount = useMountAppended(); - test('renders correctly against snapshot', () => { const children = netflowRowRenderer.renderRow({ data: getMockNetflowData(), @@ -38,8 +35,8 @@ describe('netflowRowRenderer', () => { timelineId: 'test', }); - const wrapper = shallow({children}); - expect(wrapper).toMatchSnapshot(); + const { asFragment } = render({children}); + expect(asFragment()).toMatchSnapshot(); }); describe('#isInstance', () => { @@ -106,12 +103,12 @@ describe('netflowRowRenderer', () => { isDraggable: true, timelineId: 'test', }); - const wrapper = mount( + render( {children} ); - expect(wrapper.find('[data-test-subj="destination-bytes"]').first().text()).toEqual('40B'); + expect(screen.getByText('40B')).toBeInTheDocument(); }); }); From 9d435e23c6d0fa3015609cf825cb9533da4776be Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Thu, 23 Jun 2022 17:15:12 +0200 Subject: [PATCH 35/54] [Enterprise Search] Remove doc links for unreleased connectors (#135018) --- packages/kbn-doc-links/src/get_doc_links.ts | 3 --- packages/kbn-doc-links/src/types.ts | 3 --- .../public/applications/shared/doc_links/doc_links.ts | 3 --- 3 files changed, 9 deletions(-) diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index b45e359355911..3e5c9320cb390 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -143,7 +143,6 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { jiraServer: `${WORKPLACE_SEARCH_DOCS}workplace-search-jira-server-connector.html`, networkDrive: `${WORKPLACE_SEARCH_DOCS}network-drives.html`, oneDrive: `${WORKPLACE_SEARCH_DOCS}workplace-search-onedrive-connector.html`, - outlook: `${WORKPLACE_SEARCH_DOCS}microsoft-outlook.html`, permissions: `${WORKPLACE_SEARCH_DOCS}workplace-search-permissions.html#organizational-sources-private-sources`, salesforce: `${WORKPLACE_SEARCH_DOCS}workplace-search-salesforce-connector.html`, security: `${WORKPLACE_SEARCH_DOCS}workplace-search-security.html`, @@ -152,9 +151,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { sharePointServer: `${WORKPLACE_SEARCH_DOCS}sharepoint-server.html`, slack: `${WORKPLACE_SEARCH_DOCS}workplace-search-slack-connector.html`, synch: `${WORKPLACE_SEARCH_DOCS}workplace-search-customizing-indexing-rules.html`, - teams: `${WORKPLACE_SEARCH_DOCS}microsoft-teams.html`, zendesk: `${WORKPLACE_SEARCH_DOCS}workplace-search-zendesk-connector.html`, - zoom: `${WORKPLACE_SEARCH_DOCS}zoom.html`, }, metricbeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index c6d084fabf286..3a0e57b43c169 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -129,7 +129,6 @@ export interface DocLinks { readonly jiraServer: string; readonly networkDrive: string; readonly oneDrive: string; - readonly outlook: string; readonly permissions: string; readonly salesforce: string; readonly security: string; @@ -138,9 +137,7 @@ export interface DocLinks { readonly sharePointServer: string; readonly slack: string; readonly synch: string; - readonly teams: string; readonly zendesk: string; - readonly zoom: string; }; readonly heartbeat: { readonly base: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts index 1d38cb584fa43..7d27d10a6ca85 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts @@ -264,7 +264,6 @@ class DocLinks { this.workplaceSearchJiraServer = docLinks.links.workplaceSearch.jiraServer; this.workplaceSearchNetworkDrive = docLinks.links.workplaceSearch.networkDrive; this.workplaceSearchOneDrive = docLinks.links.workplaceSearch.oneDrive; - this.workplaceSearchOutlook = docLinks.links.workplaceSearch.outlook; this.workplaceSearchPermissions = docLinks.links.workplaceSearch.permissions; this.workplaceSearchSalesforce = docLinks.links.workplaceSearch.salesforce; this.workplaceSearchSecurity = docLinks.links.workplaceSearch.security; @@ -273,9 +272,7 @@ class DocLinks { this.workplaceSearchSharePointServer = docLinks.links.workplaceSearch.sharePointServer; this.workplaceSearchSlack = docLinks.links.workplaceSearch.slack; this.workplaceSearchSynch = docLinks.links.workplaceSearch.synch; - this.workplaceSearchTeams = docLinks.links.workplaceSearch.teams; this.workplaceSearchZendesk = docLinks.links.workplaceSearch.zendesk; - this.workplaceSearchZoom = docLinks.links.workplaceSearch.zoom; } } From 74df8c1736f156781b374173e4e9016e438122f6 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 23 Jun 2022 08:25:10 -0700 Subject: [PATCH 36/54] [DOCS] Add jira connector details to run connector API (#134622) --- .../actions-and-connectors/execute.asciidoc | 130 +++++++++++++++++- 1 file changed, 128 insertions(+), 2 deletions(-) diff --git a/docs/api/actions-and-connectors/execute.asciidoc b/docs/api/actions-and-connectors/execute.asciidoc index ed66abf33f58b..97612d88ed035 100644 --- a/docs/api/actions-and-connectors/execute.asciidoc +++ b/docs/api/actions-and-connectors/execute.asciidoc @@ -50,6 +50,104 @@ depending on the connector type. For information about the parameter properties, refer to <>. + -- +.Jira connectors +[%collapsible%open] +==== +`subAction`:: +(Required, string) The action to test. Valid values include: `fieldsByIssueType`, +`getFields`, `getIncident`, `issue`, `issues`, `issueTypes`, and `pushToService`. + +`subActionParams`:: +(Required^*^, object) The set of configuration properties, which vary depending +on the `subAction` value. This object is not required when `subAction` is +`getFields` or `issueTypes`. ++ +.Properties when `subAction` is `fieldsByIssueType` +[%collapsible%open] +===== +`id`::: +(Required, string) The Jira issue type identifier. For example, `10024`. +===== ++ +.Properties when `subAction` is `getIncident` +[%collapsible%open] +===== +`externalId`::: +(Required, string) The Jira issue identifier. For example, `71778`. +===== ++ +.Properties when `subAction` is `issue` +[%collapsible%open] +===== +`id`::: +(Required, string) The Jira issue identifier. For example, `71778`. +===== ++ +.Properties when `subAction` is `issues` +[%collapsible%open] +===== +`title`::: +(Required, string) The title of the Jira issue. +===== ++ +.Properties when `subAction` is `pushToService` +[%collapsible%open] +===== +comments::: +(Optional, array of objects) Additional information that is sent to Jira. ++ +.Properties of `comments` +[%collapsible%open] +====== +comment:::: +(string) A comment related to the incident. For example, describe how to +troubleshoot the issue. + +commentId:::: +(integer) A unique identifier for the comment. +====== + +incident::: +(Required, object) Information necessary to create or update a Jira incident. ++ +.Properties of `incident` +[%collapsible%open] +====== +`description`:::: +(Optional, string) The details about the incident. + +`externalId`:::: +(Optional, string) The Jira issue identifier. If present, the incident is +updated. Otherwise, a new incident is created. + +`labels`:::: +(Optional, array of strings) The labels for the incident. For example, +`["LABEL1"]`. NOTE: Labels cannot contain spaces. + +`issueType`:::: +(Optional, integer) The type of incident. For example, `10006`. To obtain the +list of valid values, set `subAction` to `issueTypes`. + +`parent`:::: +(Optional, string) The ID or key of the parent issue. Applies only to `Sub-task` +types of issues. + +`priority`:::: +(Optional, string) The incident priority level. For example, `Lowest`. + +`summary`:::: +(Required, string) A summary of the incident. + +`title`:::: +(Optional, string) A title for the incident, used for searching the contents of +the knowledge base. +====== +===== + +For more information, refer to +{kibana-ref}/jira-action-type.html[{jira} connector and action]. +==== + .Server log connectors [%collapsible%open] ==== @@ -102,7 +200,7 @@ The API returns the following: "items": [ { "index": { - "_index": "updated-index", + "_index": "test-index", "_id": "iKyijHcBKCsmXNFrQe3T", "_version": 1, "result": "created", @@ -111,7 +209,7 @@ The API returns the following: "successful": 1, "failed": 0 }, - "_seq_no": 7, + "_seq_no": 0, "_primary_term": 1, "status": 201 } @@ -141,4 +239,32 @@ The API returns the following: [source,sh] -------------------------------------------------- {"status":"ok","connector_id":"7fc7b9a0-ecc9-11ec-8736-e7d63118c907"} +-------------------------------------------------- + +Retrieve the list of issue types for a Jira connector: + +[source,sh] +-------------------------------------------------- +POST api/actions/connector/b3aad810-edbe-11ec-82d1-11348ecbf4a6/_execute +{ + "params": { + "subAction": "issueTypes" + } +} +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "status":"ok", + "data":[ + {"id":"10024","name":"Improvement"},{"id":"10006","name":"Task"}, + {"id":"10007","name":"Sub-task"},{"id":"10025","name":"New Feature"}, + {"id":"10023","name":"Bug"},{"id":"10000","name":"Epic"} + ], + "connector_id":"b3aad810-edbe-11ec-82d1-11348ecbf4a6" +} -------------------------------------------------- \ No newline at end of file From 2e844f0c24176cd6f5afdac4d85f9f3b1dc2ca7d Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Thu, 23 Jun 2022 09:44:06 -0600 Subject: [PATCH 37/54] [Security Solution] Fixes: Queries with `nested` field types fail open with `failed to create query: [nested] failed to find nested object under path [threat.enrichments]` errors for indexes where the nested fields are unmapped (#134866) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [Security Solution] Fixes: Queries with `nested` field types fail open with `failed to create query: [nested] failed to find nested object under path [threat.enrichments]` errors for indexes where the nested fields are unmapped This PR implements a fix for , where queries with [nested](https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html) field types failed open with `failed to create query: [nested] failed to find nested object under path [threat.enrichments]` errors for indexes where the nested fields are unmapped. The fix uses the new `nestedIgnoreUnmapped` option to the `buildEsQuery` API introduced in as a fix for issue . Please see for a deep dive on the issue being fixed. ### Before ❌ Before this fix, Timeline queries that used the `nested` query syntax in requests did NOT contain the `ignore_unmapped` option, per the example request below: ```json "nested": { "path": "threat.enrichments", "query": { "bool": { "should": [ { "match": { "threat.enrichments.matched.atomic": "a4f87cbcd2a4241da77b6bf0c5d9e8553fec991f" } } ], "minimum_should_match": 1 } }, "score_mode": "none" } ``` _Above: Timeline requests for fields with the `nested` query syntax did NOT contain the `ignore_unmapped` option (when inspected)_ When indexes where the nested fields were unmapped were searched: - Elasticsearch returned a `200` status code - The response from Elasticsearch included shard failures, per the example response below: ```json "_shards": { "total": 5, "successful": 3, "skipped": 0, "failed": 2, "failures": [ { "shard": 0, "index": ".ds-logs-endpoint.events.process-default-2022.06.13-000001", "node": "3nAChOVOQKy92bhuDztcgA", "reason": { "type": "query_shard_exception", "reason": "failed to create query: [nested] failed to find nested object under path [threat.enrichments]", ``` _Above: Timeline responses contained shard failures (when inspected)_ ### After ✅ After this fix, Timeline queries that use the `nested` syntax in requests contain the `"ignore_unmapped": true` option, per the example request below: ```json "nested": { "path": "threat.enrichments", "query": { "bool": { "should": [ { "match": { "threat.enrichments.matched.atomic": "a4f87cbcd2a4241da77b6bf0c5d9e8553fec991f" } } ], "minimum_should_match": 1 } }, "score_mode": "none", "ignore_unmapped": true } ``` _Above: Timeline requests with the `nested` query syntax `"ignore_unmapped": true` option (when inspected)_ When indexes where the nested fields were unmapped are searched: - Elasticsearch (still) returs a `200` status code - The response from Elasticsearch does NOT include shard failures, per the example response below: ```json "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, ``` ### A tail of two `convertToBuildEsQuery` functions While fixing this PR, it was noted that there are two different implementations of the `convertToBuildEsQuery` function in: - `x-pack/plugins/security_solution/public/common/lib/keury/index.ts` - `x-pack/plugins/timelines/public/components/utils/keury/index.ts` The implementations of these functions are not the same. Specifically, the return type of the former implementation is: ```ts [string, undefined] | [undefined, Error] ``` and the latter is just: ```ts string ``` - This PR reduces the implementations of `convertToBuildEsQuery` down to a single function exported by the `timelines` plugin in `x-pack/plugins/timelines/public/components/utils/keury/index.ts` - To minimize the scope of the changes in this PR, the previous Security Solution implementation in `x-pack/plugins/security_solution/public/common/lib/keury/index.ts` re-exports the new `timelines` implementation. ### Desk testing See the _Reproduction steps_ section of for details --- .../public/common/lib/keury/index.test.ts | 65 ----- .../public/common/lib/keury/index.ts | 97 +------ .../public/components/t_grid/helpers.tsx | 55 +++- .../components/utils/keury/index.test.ts | 272 +++++++++++++++++- .../public/components/utils/keury/index.ts | 32 ++- x-pack/plugins/timelines/public/index.ts | 7 + 6 files changed, 352 insertions(+), 176 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/common/lib/keury/index.test.ts diff --git a/x-pack/plugins/security_solution/public/common/lib/keury/index.test.ts b/x-pack/plugins/security_solution/public/common/lib/keury/index.test.ts deleted file mode 100644 index 936053a18be5c..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/keury/index.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { escapeKuery } from '.'; - -describe('Kuery escape', () => { - it('should not remove white spaces quotes', () => { - const value = ' netcat'; - const expected = ' netcat'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should escape quotes', () => { - const value = 'I said, "Hello."'; - const expected = 'I said, \\"Hello.\\"'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should escape special characters', () => { - const value = `This \\ has (a lot of) characters, don't you *think*? "Yes."`; - const expected = `This \\ has (a lot of) characters, don't you *think*? \\"Yes.\\"`; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should NOT escape keywords', () => { - const value = 'foo and bar or baz not qux'; - const expected = 'foo and bar or baz not qux'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should NOT escape keywords next to each other', () => { - const value = 'foo and bar or not baz'; - const expected = 'foo and bar or not baz'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should not escape keywords without surrounding spaces', () => { - const value = 'And this has keywords, or does it not?'; - const expected = 'And this has keywords, or does it not?'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should NOT escape uppercase keywords', () => { - const value = 'foo AND bar'; - const expected = 'foo AND bar'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should escape special characters and NOT keywords', () => { - const value = 'Hello, "world", and to meet you!'; - const expected = 'Hello, \\"world\\", and to meet you!'; - expect(escapeKuery(value)).to.be(expected); - }); - - it('should escape newlines and tabs', () => { - const value = 'This\nhas\tnewlines\r\nwith\ttabs'; - const expected = 'This\\nhas\\tnewlines\\r\\nwith\\ttabs'; - expect(escapeKuery(value)).to.be(expected); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/lib/keury/index.ts b/x-pack/plugins/security_solution/public/common/lib/keury/index.ts index b9f2f9a297bce..735996d762ffc 100644 --- a/x-pack/plugins/security_solution/public/common/lib/keury/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/keury/index.ts @@ -5,93 +5,10 @@ * 2.0. */ -import { isEmpty, isString, flow } from 'lodash/fp'; - -import { - EsQueryConfig, - Query, - Filter, - buildEsQuery, - toElasticsearchQuery, - fromKueryExpression, - DataViewBase, -} from '@kbn/es-query'; - -export const convertKueryToElasticSearchQuery = ( - kueryExpression: string, - indexPattern?: DataViewBase -) => { - try { - return kueryExpression - ? JSON.stringify(toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern)) - : ''; - } catch (err) { - return ''; - } -}; - -export const convertKueryToDslFilter = (kueryExpression: string, indexPattern: DataViewBase) => { - try { - return kueryExpression - ? toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern) - : {}; - } catch (err) { - return {}; - } -}; - -export const escapeQueryValue = (val: number | string = ''): string | number => { - if (isString(val)) { - if (isEmpty(val)) { - return '""'; - } - return `"${escapeKuery(val)}"`; - } - - return val; -}; - -const escapeWhitespace = (val: string) => - val.replace(/\t/g, '\\t').replace(/\r/g, '\\r').replace(/\n/g, '\\n'); - -// See the SpecialCharacter rule in kuery.peg -const escapeSpecialCharacters = (val: string) => val.replace(/["]/g, '\\$&'); // $& means the whole matched string - -// See the Keyword rule in kuery.peg -// I do not think that we need that anymore since we are doing a full match_phrase all the time now => return `"${escapeKuery(val)}"`; -// const escapeAndOr = (val: string) => val.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3'); - -// const escapeNot = (val: string) => val.replace(/not(\s+)/gi, '\\$&'); - -export const escapeKuery = flow(escapeSpecialCharacters, escapeWhitespace); - -export const convertToBuildEsQuery = ({ - config, - indexPattern, - queries, - filters, -}: { - config: EsQueryConfig; - indexPattern: DataViewBase; - queries: Query[]; - filters: Filter[]; -}): [string, undefined] | [undefined, Error] => { - try { - return [ - JSON.stringify( - buildEsQuery( - indexPattern, - queries, - filters.filter((f) => f.meta.disabled === false), - { - ...config, - dateFormatTZ: undefined, - } - ) - ), - undefined, - ]; - } catch (error) { - return [undefined, error]; - } -}; +export { + convertKueryToDslFilter, + convertKueryToElasticSearchQuery, + convertToBuildEsQuery, + escapeKuery, + escapeQueryValue, +} from '@kbn/timelines-plugin/public'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx index 6d738c49e5b6b..03830339761bc 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx @@ -153,27 +153,61 @@ export const combineQueries = ({ kqlQuery, kqlMode, isEventViewer, -}: CombineQueries): { filterQuery: string } | null => { +}: CombineQueries): { filterQuery: string | undefined; kqlError: Error | undefined } | null => { const kuery: Query = { query: '', language: kqlQuery.language }; if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEmpty(filters) && !isEventViewer) { return null; } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEventViewer) { + const [filterQuery, kqlError] = convertToBuildEsQuery({ + config, + queries: [kuery], + indexPattern, + filters, + }); + return { - filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + filterQuery, + kqlError, }; } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && !isEmpty(filters)) { + const [filterQuery, kqlError] = convertToBuildEsQuery({ + config, + queries: [kuery], + indexPattern, + filters, + }); + return { - filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + filterQuery, + kqlError, }; } else if (isEmpty(dataProviders) && !isEmpty(kqlQuery.query)) { kuery.query = `(${kqlQuery.query})`; + + const [filterQuery, kqlError] = convertToBuildEsQuery({ + config, + queries: [kuery], + indexPattern, + filters, + }); + return { - filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + filterQuery, + kqlError, }; } else if (!isEmpty(dataProviders) && isEmpty(kqlQuery)) { kuery.query = `(${buildGlobalQuery(dataProviders, browserFields)})`; + + const [filterQuery, kqlError] = convertToBuildEsQuery({ + config, + queries: [kuery], + indexPattern, + filters, + }); + return { - filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + filterQuery, + kqlError, }; } const operatorKqlQuery = kqlMode === 'filter' ? 'and' : 'or'; @@ -181,8 +215,17 @@ export const combineQueries = ({ kuery.query = `((${buildGlobalQuery(dataProviders, browserFields)})${postpend( kqlQuery.query as string )})`; + + const [filterQuery, kqlError] = convertToBuildEsQuery({ + config, + queries: [kuery], + indexPattern, + filters, + }); + return { - filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + filterQuery, + kqlError, }; }; diff --git a/x-pack/plugins/timelines/public/components/utils/keury/index.test.ts b/x-pack/plugins/timelines/public/components/utils/keury/index.test.ts index 936053a18be5c..5f5147999b872 100644 --- a/x-pack/plugins/timelines/public/components/utils/keury/index.test.ts +++ b/x-pack/plugins/timelines/public/components/utils/keury/index.test.ts @@ -6,7 +6,8 @@ */ import expect from '@kbn/expect'; -import { escapeKuery } from '.'; +import { convertToBuildEsQuery, escapeKuery } from '.'; +import { mockIndexPattern } from '../../../mock/index_pattern'; describe('Kuery escape', () => { it('should not remove white spaces quotes', () => { @@ -63,3 +64,272 @@ describe('Kuery escape', () => { expect(escapeKuery(value)).to.be(expected); }); }); + +describe('convertToBuildEsQuery', () => { + /** + * All the fields in this query, except for `@timestamp`, + * are nested fields https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html + * + * This mix of nested and non-nested fields will be used to verify that: + * ✅ Nested fields are converted to use the `nested` query syntax + * ✅ The `nested` query syntax includes the `ignore_unmapped` option + * ✅ Non-nested fields are NOT converted to the `nested` query syntax + * ✅ Non-nested fields do NOT include the `ignore_unmapped` option + */ + const queryWithNestedFields = [ + { + query: + '((threat.enrichments: { matched.atomic: a4f87cbcd2a4241da77b6bf0c5d9e8553fec991f } and threat.enrichments: { matched.type: indicator_match_rule } and threat.enrichments: { matched.field: file.hash.md5 }) and (@timestamp : *))', + language: 'kuery', + }, + ]; + + /** A search bar filter (displayed below the KQL / Lucene search bar ) */ + const filters = [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'exists', + key: '_id', + value: 'exists', + }, + query: { + exists: { + field: '_id', + }, + }, + }, + ]; + + const config = { + allowLeadingWildcards: true, + queryStringOptions: { + analyze_wildcard: true, + }, + ignoreFilterIfFieldNotInIndex: false, + dateFormatTZ: 'Browser', + }; + + it('should, by default, build a query where the `nested` fields syntax includes the `"ignore_unmapped":true` option', () => { + const [converted, _] = convertToBuildEsQuery({ + config, + queries: queryWithNestedFields, + indexPattern: mockIndexPattern, + filters, + }); + + expect(JSON.parse(converted ?? '')).to.eql({ + bool: { + must: [], + filter: [ + { + bool: { + filter: [ + { + bool: { + filter: [ + { + // ✅ Nested fields are converted to use the `nested` query syntax + nested: { + path: 'threat.enrichments', + query: { + bool: { + should: [ + { + match: { + 'threat.enrichments.matched.atomic': + 'a4f87cbcd2a4241da77b6bf0c5d9e8553fec991f', + }, + }, + ], + minimum_should_match: 1, + }, + }, + score_mode: 'none', + // ✅ The `nested` query syntax includes the `ignore_unmapped` option + ignore_unmapped: true, + }, + }, + { + nested: { + path: 'threat.enrichments', + query: { + bool: { + should: [ + { + match: { + 'threat.enrichments.matched.type': 'indicator_match_rule', + }, + }, + ], + minimum_should_match: 1, + }, + }, + score_mode: 'none', + ignore_unmapped: true, + }, + }, + { + nested: { + path: 'threat.enrichments', + query: { + bool: { + should: [ + { + match: { + 'threat.enrichments.matched.field': 'file.hash.md5', + }, + }, + ], + minimum_should_match: 1, + }, + }, + score_mode: 'none', + ignore_unmapped: true, + }, + }, + ], + }, + }, + { + bool: { + should: [ + { + exists: { + // ✅ Non-nested fields are NOT converted to the `nested` query syntax + // ✅ Non-nested fields do NOT include the `ignore_unmapped` option + field: '@timestamp', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { + exists: { + field: '_id', + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); + + it('should, when the default is overridden, build a query where `nested` fields include the `"ignore_unmapped":false` option', () => { + const configWithOverride = { + ...config, + nestedIgnoreUnmapped: false, // <-- override the default + }; + + const [converted, _] = convertToBuildEsQuery({ + config: configWithOverride, + queries: queryWithNestedFields, + indexPattern: mockIndexPattern, + filters, + }); + + expect(JSON.parse(converted ?? '')).to.eql({ + bool: { + must: [], + filter: [ + { + bool: { + filter: [ + { + bool: { + filter: [ + { + nested: { + path: 'threat.enrichments', + query: { + bool: { + should: [ + { + match: { + 'threat.enrichments.matched.atomic': + 'a4f87cbcd2a4241da77b6bf0c5d9e8553fec991f', + }, + }, + ], + minimum_should_match: 1, + }, + }, + score_mode: 'none', + ignore_unmapped: false, // <-- overridden by the config to be false + }, + }, + { + nested: { + path: 'threat.enrichments', + query: { + bool: { + should: [ + { + match: { + 'threat.enrichments.matched.type': 'indicator_match_rule', + }, + }, + ], + minimum_should_match: 1, + }, + }, + score_mode: 'none', + ignore_unmapped: false, + }, + }, + { + nested: { + path: 'threat.enrichments', + query: { + bool: { + should: [ + { + match: { + 'threat.enrichments.matched.field': 'file.hash.md5', + }, + }, + ], + minimum_should_match: 1, + }, + }, + score_mode: 'none', + ignore_unmapped: false, + }, + }, + ], + }, + }, + { + bool: { + should: [ + { + exists: { + field: '@timestamp', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { + exists: { + field: '_id', + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/utils/keury/index.ts b/x-pack/plugins/timelines/public/components/utils/keury/index.ts index 776f5883a57d5..240800c0af75f 100644 --- a/x-pack/plugins/timelines/public/components/utils/keury/index.ts +++ b/x-pack/plugins/timelines/public/components/utils/keury/index.ts @@ -74,20 +74,24 @@ export const convertToBuildEsQuery = ({ indexPattern: DataViewBase; queries: Query[]; filters: Filter[]; -}) => { +}): [string, undefined] | [undefined, Error] => { try { - return JSON.stringify( - buildEsQuery( - indexPattern, - queries, - filters.filter((f) => f.meta.disabled === false), - { - ...config, - dateFormatTZ: undefined, - } - ) - ); - } catch (exp) { - return ''; + return [ + JSON.stringify( + buildEsQuery( + indexPattern, + queries, + filters.filter((f) => f.meta.disabled === false), + { + nestedIgnoreUnmapped: true, // by default, prevent shard failures when unmapped `nested` fields are queried: https://github.com/elastic/kibana/issues/130340 + ...config, + dateFormatTZ: undefined, + } + ) + ), + undefined, + ]; + } catch (error) { + return [undefined, error]; } }; diff --git a/x-pack/plugins/timelines/public/index.ts b/x-pack/plugins/timelines/public/index.ts index 09b66f8389f10..f6dd148c8dce3 100644 --- a/x-pack/plugins/timelines/public/index.ts +++ b/x-pack/plugins/timelines/public/index.ts @@ -77,6 +77,13 @@ export { getActionsColumnWidth } from './components/t_grid/body/column_headers/h export { DEFAULT_ACTION_BUTTON_WIDTH } from './components/t_grid/body/constants'; export { useBulkActionItems } from './hooks/use_bulk_action_items'; export { getPageRowIndex } from '../common/utils/pagination'; +export { + convertKueryToDslFilter, + convertKueryToElasticSearchQuery, + convertToBuildEsQuery, + escapeKuery, + escapeQueryValue, +} from './components/utils/keury'; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. From d00387e7b402b350f20e1d0b51383b2ce712119a Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Thu, 23 Jun 2022 18:01:18 +0200 Subject: [PATCH 38/54] Add connector service package option to Content plugin (#135029) --- .../common/types/connector_package.ts | 12 + .../add_connector_package_api_logic.ts | 30 +++ .../components/new_index/method_api.tsx | 1 + .../components/new_index/method_connector.tsx | 29 +-- .../components/new_index/method_crawler.tsx | 1 + .../components/new_index/method_es.tsx | 1 + .../components/new_index/method_json.tsx | 1 + .../components/new_index/new_index.tsx | 43 +--- .../new_index/new_search_index_template.tsx | 233 ++++++++++-------- .../plugins/enterprise_search/server/index.ts | 2 + .../server/lib/connectors/add_connector.ts | 66 +++++ .../enterprise_search/server/plugin.ts | 2 + .../routes/enterprise_search/connectors.ts | 37 +++ 13 files changed, 302 insertions(+), 156 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/common/types/connector_package.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector_package/add_connector_package_api_logic.ts create mode 100644 x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts diff --git a/x-pack/plugins/enterprise_search/common/types/connector_package.ts b/x-pack/plugins/enterprise_search/common/types/connector_package.ts new file mode 100644 index 0000000000000..2051360b733f4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/common/types/connector_package.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface ConnectorPackage { + id: string; + indexName: string; + name: string; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector_package/add_connector_package_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector_package/add_connector_package_api_logic.ts new file mode 100644 index 0000000000000..fdf9d00c3e0bc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector_package/add_connector_package_api_logic.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 { createApiLogic } from '../../../shared/api_logic/create_api_logic'; +import { HttpLogic } from '../../../shared/http'; + +interface AddConnectorValue { + id: string; + apiKey: string; +} + +const addConnectorPackage = async ({ indexName }: { indexName: string }) => { + const route = '/internal/enterprise_search/connectors'; + + const params = { + index_name: indexName, + }; + return await HttpLogic.values.http.post(route, { + body: JSON.stringify(params), + }); +}; + +export const AddConnectorPackageApiLogic = createApiLogic( + ['add_connector_package_api_logic'], + addConnectorPackage +); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_api.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_api.tsx index a68c33db0eae5..bfe9c6293da03 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_api.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_api.tsx @@ -36,6 +36,7 @@ export const MethodApi: React.FC = () => { } docsUrl="#" type="api" + onSubmit={() => null} /> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_connector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_connector.tsx index 36522002d7e88..0a79bb1600d35 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_connector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_connector.tsx @@ -13,15 +13,21 @@ import React from 'react'; -import { EuiPanel, EuiTitle } from '@elastic/eui'; +import { useActions, useValues } from 'kea'; + import { i18n } from '@kbn/i18n'; +import { Status } from '../../../../../common/types/api'; +import { AddConnectorPackageApiLogic } from '../../api/connector_package/add_connector_package_api_logic'; + import { NewSearchIndexTemplate } from './new_search_index_template'; export const MethodConnector: React.FC = () => { + const { makeRequest } = useActions(AddConnectorPackageApiLogic); + const { status } = useValues(AddConnectorPackageApiLogic); return ( { )} docsUrl="#" type="connector" - > - - -

Place the connector flow here...

-
-
-
+ onSubmit={(name) => makeRequest({ indexName: name })} + formDisabled={status === Status.LOADING} + buttonLoading={status === Status.LOADING} + /> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_crawler.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_crawler.tsx index 1985cf47629ea..8f46e5e0dc0e9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_crawler.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/method_crawler.tsx @@ -31,6 +31,7 @@ export const MethodCrawler: React.FC = () => { )} docsUrl="#" type="crawler" + onSubmit={() => null} > { docsUrl="#" type="elasticsearch" onNameChange={(value: string) => onNameChange(value)} + onSubmit={() => null} > { docsUrl="#" type="json" onNameChange={(value: string) => onNameChange(value)} + onSubmit={() => null} > { const [selectedMethod, setSelectedMethod] = useState({ id: '', label: '' }); - const [methodIsSelected, setMethodIsSelected] = useState(false); const { loadSearchEngines } = useActions(SearchIndicesLogic); useEffect(() => { loadSearchEngines(); }, []); - const buttonGroupOptions = [ + const buttonGroupOptions: ButtonGroupOption[] = [ { id: 'crawler', icon: 'globe', @@ -70,20 +69,6 @@ export const NewIndex: React.FC = () => { } ), }, - { - id: 'connector', - icon: 'package', - label: i18n.translate('xpack.enterpriseSearch.content.newIndex.buttonGroup.connector.label', { - defaultMessage: 'Use a data integration', - }), - description: i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.buttonGroup.connector.description', - { - defaultMessage: - 'Index content frrom third-party services such as SharePoint and Google Drive', - } - ), - }, { id: 'api', icon: 'visVega', @@ -98,27 +83,25 @@ export const NewIndex: React.FC = () => { ), }, { - id: 'customIntegration', + id: 'connector', icon: 'package', - label: i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.buttonGroup.customIntegration.label', - { - defaultMessage: 'Build a custom data integration', - } - ), + label: i18n.translate('xpack.enterpriseSearch.content.newIndex.buttonGroup.connector.label', { + defaultMessage: 'Build a connector package', + }), description: i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.buttonGroup.customIntegration.description', + 'xpack.enterpriseSearch.content.newIndex.buttonGroup.connector.description', { - defaultMessage: 'Clone the connector package repo and start customizing.', + defaultMessage: 'Clone the connector package repo and build a custom connector', } ), }, - ] as ButtonGroupOption[]; + ]; const handleMethodChange = (val: string) => { - const selected = buttonGroupOptions.find((b) => b.id === val) as ButtonGroupOption; - setSelectedMethod(selected); - setMethodIsSelected(true); + const selected = buttonGroupOptions.find((b) => b.id === val); + if (selected) { + setSelectedMethod(selected); + } }; const NewSearchIndexLayout = () => ( @@ -198,7 +181,7 @@ export const NewIndex: React.FC = () => {
- {methodIsSelected ? : } + {selectedMethod ? : }
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/new_search_index_template.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/new_search_index_template.tsx index 1eb228aa876e0..6252b28a54b38 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/new_search_index_template.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/new_search_index_template.tsx @@ -20,6 +20,7 @@ import { EuiFieldText, EuiFlexGroup, EuiFlexItem, + EuiForm, EuiFormRow, EuiLink, EuiPanel, @@ -31,29 +32,28 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Engine } from '../../../app_search/components/engine/types'; - import { SUPPORTED_LANGUAGES } from './constants'; import { NewSearchIndexLogic } from './new_search_index_logic'; -export interface ISearchIndex { +interface SearchIndex { title: React.ReactNode; description: React.ReactNode; docsUrl: string; type: string; onNameChange?(name: string): void; + onSubmit(name: string): void; + buttonLoading?: boolean; + formDisabled?: boolean; } -export interface ISearchEngineOption { - label: string; - value: Engine; -} - -export const NewSearchIndexTemplate: React.FC = ({ +export const NewSearchIndexTemplate: React.FC = ({ children, title, description, onNameChange, + onSubmit, + formDisabled, + buttonLoading, }) => { const { name, language, rawName } = useValues(NewSearchIndexLogic); const { setRawName, setLanguage } = useActions(NewSearchIndexLogic); @@ -71,110 +71,125 @@ export const NewSearchIndexTemplate: React.FC = ({ return ( - - - -

{title}

-
- -

- {description} - - {i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.learnMore.linkText', - { - defaultMessage: 'Learn more', - } - )} - -

-
-
- - - - 0 ? `Your index will be named: ${name}` : '', - }, - } - )} - fullWidth - > - { + event.preventDefault(); + onSubmit(name); + }} + component="form" + id="enterprise-search-add-connector" + > + + + +

{title}

+
+ +

+ {description} + + {i18n.translate( + 'xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.learnMore.linkText', + { + defaultMessage: 'Learn more', + } + )} + +

+
+
+ + + + + 0 ? `Your index will be named: ${name}` : '', + }, } )} fullWidth - isInvalid={false} - value={rawName} - onChange={handleNameChange} - /> - - - - - - - - - - {children} -
- - - - - {i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.createIndex.buttonText', - { - defaultMessage: 'Create index', - } - )} - - - - - {i18n.translate( - 'xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.viewDocumentation.linkText', - { - defaultMessage: 'View the documentation', - } - )} - - - + > + +
+
+ + + + + +
+
+ {children} +
+ + + + + {i18n.translate( + 'xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.createIndex.buttonText', + { + defaultMessage: 'Create index', + } + )} + + + + + {i18n.translate( + 'xpack.enterpriseSearch.content.newIndex.newSearchIndexTemplate.viewDocumentation.linkText', + { + defaultMessage: 'View the documentation', + } + )} + + + + = { host: true, }, }; + +export const CONNECTORS_INDEX = '.ent-search-connectors'; diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts new file mode 100644 index 0000000000000..bf17417856e01 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts @@ -0,0 +1,66 @@ +/* + * 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 { IScopedClusterClient } from '@kbn/core/server'; + +import { CONNECTORS_INDEX } from '../..'; + +interface ConnectorDocument { + index_name: string; +} + +export const createConnectorsIndex = async (client: IScopedClusterClient): Promise => { + const index = CONNECTORS_INDEX; + await client.asCurrentUser.indices.create({ index }); +}; + +const createConnector = async ( + index: string, + document: ConnectorDocument, + client: IScopedClusterClient +): Promise<{ id: string; apiKey: string }> => { + const result = await client.asCurrentUser.index({ + index, + document, + }); + await client.asCurrentUser.indices.create({ index: document.index_name }); + const apiKeyResult = await client.asCurrentUser.security.createApiKey({ + name: `${document.index_name}-connector`, + role_descriptors: { + [`${document.index_name}-connector-name`]: { + cluster: [], + index: [ + { + names: [document.index_name, index], + privileges: ['all'], + }, + ], + }, + }, + }); + return { apiKey: apiKeyResult.encoded, id: result._id }; +}; + +export const addConnector = async ( + client: IScopedClusterClient, + document: ConnectorDocument +): Promise<{ apiKey: string; id: string }> => { + const index = CONNECTORS_INDEX; + try { + return createConnector(index, document, client); + } catch (error) { + if (error.statusCode === 404) { + // This means .ent-search-connectors index doesn't exist yet + // So we first have to create it, and then try inserting the document again + // TODO: Move index creation to Kibana startup instead + await createConnectorsIndex(client); + return createConnector(index, document, client); + } else { + throw error; + } + } +}; diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 110adec8bc211..faf6497ed0110 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -46,6 +46,7 @@ import { import { registerAppSearchRoutes } from './routes/app_search'; import { registerConfigDataRoute } from './routes/enterprise_search/config_data'; +import { registerConnectorRoutes } from './routes/enterprise_search/connectors'; import { registerIndexRoutes } from './routes/enterprise_search/indices'; import { registerTelemetryRoute } from './routes/enterprise_search/telemetry'; import { registerWorkplaceSearchRoutes } from './routes/workplace_search'; @@ -161,6 +162,7 @@ export class EnterpriseSearchPlugin implements Plugin { registerAppSearchRoutes(dependencies); registerWorkplaceSearchRoutes(dependencies); registerIndexRoutes(dependencies); + registerConnectorRoutes(dependencies); /** * Bootstrap the routes, saved objects, and collector for telemetry diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts new file mode 100644 index 0000000000000..64921f9207a93 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +import { addConnector } from '../../lib/connectors/add_connector'; + +import { RouteDependencies } from '../../plugin'; + +export function registerConnectorRoutes({ router }: RouteDependencies) { + router.post( + { + path: '/internal/enterprise_search/connectors', + validate: { + body: schema.object({ + index_name: schema.string(), + }), + }, + }, + async (context, request, response) => { + const { client } = (await context.core).elasticsearch; + try { + const body = await addConnector(client, request.body); + return response.ok({ body }); + } catch (error) { + return response.customError({ + statusCode: 502, + body: 'Error fetching data from Enterprise Search', + }); + } + } + ); +} From df786126a4849cf672d0c8da4f4918642af69a9f Mon Sep 17 00:00:00 2001 From: Giorgos Bamparopoulos Date: Thu, 23 Jun 2022 17:19:08 +0100 Subject: [PATCH 39/54] [APM]] Fix broken link to APM (#134665) * Fix broken link to APM traces --- .../public/components/routing/redirect_to.tsx | 2 +- .../shared/is_route_with_time_range.ts | 24 +++ .../redirect_with_offset/index.test.tsx | 160 ++++++++++++++++++ .../shared/redirect_with_offset/index.tsx | 22 +-- 4 files changed, 197 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/shared/redirect_with_offset/index.test.tsx diff --git a/x-pack/plugins/apm/public/components/routing/redirect_to.tsx b/x-pack/plugins/apm/public/components/routing/redirect_to.tsx index 7e5e01cadbf3e..aeedf5f9cb55b 100644 --- a/x-pack/plugins/apm/public/components/routing/redirect_to.tsx +++ b/x-pack/plugins/apm/public/components/routing/redirect_to.tsx @@ -37,7 +37,7 @@ interface Props { * Given a pathname, redirect to that location, preserving the search and maintaining * backward-compatibilty with legacy (pre-7.9) hash-based URLs. */ -function RenderRedirectTo(props: Props) { +export function RenderRedirectTo(props: Props) { const { location } = props; let search = location.search; let pathname = props.pathname; diff --git a/x-pack/plugins/apm/public/components/shared/is_route_with_time_range.ts b/x-pack/plugins/apm/public/components/shared/is_route_with_time_range.ts index 9d210fe6bd059..81aa567140ed4 100644 --- a/x-pack/plugins/apm/public/components/shared/is_route_with_time_range.ts +++ b/x-pack/plugins/apm/public/components/shared/is_route_with_time_range.ts @@ -31,3 +31,27 @@ export function isRouteWithTimeRange({ return matchesRoute; } + +export function isRouteWithComparison({ + apmRouter, + location, +}: { + apmRouter: ApmRouter; + location: Location; +}) { + const matchingRoutes = apmRouter.getRoutesToMatch(location.pathname); + const matchesRoute = matchingRoutes.some((route) => { + return ( + route.path === '/services' || + route.path === '/service-map' || + route.path === '/backends' || + route.path === '/backends/inventory' || + route.path === '/services/{serviceName}' || + route.path === '/service-groups' || + location.pathname === '/' || + location.pathname === '' + ); + }); + + return matchesRoute; +} diff --git a/x-pack/plugins/apm/public/components/shared/redirect_with_offset/index.test.tsx b/x-pack/plugins/apm/public/components/shared/redirect_with_offset/index.test.tsx new file mode 100644 index 0000000000000..a47d3c76b500b --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/redirect_with_offset/index.test.tsx @@ -0,0 +1,160 @@ +/* + * 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 React from 'react'; +import { RouterProvider } from '@kbn/typed-react-router-config'; +import { render } from '@testing-library/react'; +import { createMemoryHistory, Location, MemoryHistory } from 'history'; +import qs from 'query-string'; +import { RedirectWithOffset } from '.'; +import { apmRouter } from '../../routing/apm_route_config'; +import * as useApmPluginContextExports from '../../../context/apm_plugin/use_apm_plugin_context'; +import { TimeRangeComparisonEnum } from '../time_comparison/get_comparison_options'; + +describe('RedirectWithOffset', () => { + let history: MemoryHistory; + + beforeEach(() => { + history = createMemoryHistory(); + }); + + function renderUrl( + location: Pick, + defaultSetting: boolean + ) { + history.replace(location); + + jest + .spyOn(useApmPluginContextExports, 'useApmPluginContext') + .mockReturnValue({ + core: { + uiSettings: { + get: () => defaultSetting, + }, + }, + } as any); + + return render( + + + <>Foo + + + ); + } + + it('eventually renders the child element', async () => { + const element = renderUrl( + { pathname: '/services', search: location.search, hash: '' }, + false + ); + + await expect(element.findByText('Foo')).resolves.not.toBeUndefined(); + + // assertion to make sure our element test actually works + await expect(element.findByText('Bar')).rejects.not.toBeUndefined(); + }); + + it('redirects with comparisonEnabled=false when comparison is disabled in advanced settings', async () => { + renderUrl( + { pathname: '/services', search: location.search, hash: '' }, + false + ); + + const query = qs.parse(history.entries[0].search); + expect(query.comparisonEnabled).toBe('false'); + }); + + it('redirects with comparisonEnabled=true when comparison is enabled in advanced settings', async () => { + renderUrl( + { pathname: '/services', search: location.search, hash: '' }, + true + ); + + const query = qs.parse(history.entries[0].search); + expect(query.comparisonEnabled).toBe('true'); + }); + + it('does not redirect when comparisonEnabled is defined in the url', async () => { + renderUrl( + { + pathname: '/services', + search: qs.stringify({ + comparisonEnabled: 'false', + }), + hash: '', + }, + true + ); + + const query = qs.parse(history.entries[0].search); + expect(query.comparisonEnabled).toBe('false'); + }); + + it('redirects with offset=1d when comparisonType=day is set in the query params', () => { + renderUrl( + { + pathname: '/services', + search: qs.stringify({ + comparisonType: TimeRangeComparisonEnum.DayBefore, + }), + hash: '', + }, + true + ); + + const query = qs.parse(history.entries[0].search); + expect(query.offset).toBe('1d'); + }); + + it('redirects with offset=1w when comparisonType=week is set in the query params', () => { + renderUrl( + { + pathname: '/services', + search: qs.stringify({ + comparisonType: TimeRangeComparisonEnum.WeekBefore, + }), + hash: '', + }, + true + ); + + const query = qs.parse(history.entries[0].search); + expect(query.offset).toBe('1w'); + }); + + it('redirects without offset when comparisonType=period is set in the query params', () => { + renderUrl( + { + pathname: '/services', + search: qs.stringify({ + comparisonType: TimeRangeComparisonEnum.PeriodBefore, + }), + hash: '', + }, + true + ); + + const query = qs.parse(history.entries[0].search); + expect(query.offset).toBeUndefined(); + }); + + it('without offset when comparisonType=period is set in the query params', () => { + renderUrl( + { + pathname: '', + search: qs.stringify({ + comparisonType: TimeRangeComparisonEnum.PeriodBefore, + }), + hash: 'services', + }, + true + ); + + const query = qs.parse(history.entries[0].search); + expect(query.offset).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/redirect_with_offset/index.tsx b/x-pack/plugins/apm/public/components/shared/redirect_with_offset/index.tsx index fb5c98ba515fa..268735128659d 100644 --- a/x-pack/plugins/apm/public/components/shared/redirect_with_offset/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/redirect_with_offset/index.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import { useLocation, Redirect } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import qs from 'query-string'; import React from 'react'; import { useApmRouter } from '../../../hooks/use_apm_router'; -import { isRouteWithTimeRange } from '../is_route_with_time_range'; +import { isRouteWithComparison } from '../is_route_with_time_range'; import { TimeRangeComparisonEnum, dayAndWeekBeforeToOffset, @@ -17,6 +17,7 @@ import { import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { getComparisonEnabled } from '../time_comparison/get_comparison_enabled'; import { toBoolean } from '../../../context/url_params_context/helpers'; +import { RenderRedirectTo } from '../../routing/redirect_to'; export function RedirectWithOffset({ children, @@ -26,7 +27,7 @@ export function RedirectWithOffset({ const { core } = useApmPluginContext(); const location = useLocation(); const apmRouter = useApmRouter(); - const matchesRoute = isRouteWithTimeRange({ apmRouter, location }); + const matchesRoute = isRouteWithComparison({ apmRouter, location }); const query = qs.parse(location.search); // Redirect when 'comparisonType' is set as we now use offset instead @@ -55,15 +56,16 @@ export function RedirectWithOffset({ const dayOrWeekOffset = dayAndWeekBeforeToOffset[comparisonTypeEnumValue]; return ( - ); } From a3c719d9583a06322cd2d9ea765c2d1976d2bd82 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Thu, 23 Jun 2022 19:49:48 +0300 Subject: [PATCH 40/54] [Canvas] Fixes Canvas filter behaviour on table. (#134801) * Fixed the problem with picking the absent page on filter change. * Fixed weird behavior after the empty table. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/canvas/public/components/paginate/index.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/canvas/public/components/paginate/index.tsx b/x-pack/plugins/canvas/public/components/paginate/index.tsx index 12717a07f4cf9..5998b68ffd8bd 100644 --- a/x-pack/plugins/canvas/public/components/paginate/index.tsx +++ b/x-pack/plugins/canvas/public/components/paginate/index.tsx @@ -27,7 +27,7 @@ export const Paginate: React.FunctionComponent = ({ const initialCurrentPage = totalPages > 0 ? Math.min(startPage, totalPages - 1) : 0; const [currentPage, setPage] = useState(initialCurrentPage); const hasRenderedRef = useRef(false); - const maxPage = totalPages - 1; + const maxPage = Math.min(totalPages - 1, 0); const start = currentPage * perPage; const end = currentPage === 0 ? perPage : perPage * (currentPage + 1); const nextPageEnabled = currentPage < maxPage; @@ -54,6 +54,12 @@ export const Paginate: React.FunctionComponent = ({ } }, [perPage, hasRenderedRef]); + useEffect(() => { + if (currentPage > maxPage) { + setPage(maxPage); + } + }, [currentPage, maxPage]); + return ( Date: Thu, 23 Jun 2022 17:53:58 +0100 Subject: [PATCH 41/54] [APM] Add OpenTelemetry instructions to APM tutorial (#134451) * Add config settings for OpenTelemetry to the APM tutorial Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Brandon Morelli --- .../home/common/instruction_variant.ts | 2 + .../instructions/apm_agent_instructions.ts | 43 +++++ .../read_only_user/tutorial/tutorial.spec.ts | 36 +++++ .../agent_instructions_accordion.tsx | 26 +-- .../apm_agents/agent_instructions_mappings.ts | 7 + .../agent_config_instructions.test.tsx | 59 +++++++ .../agent_config_instructions.tsx | 50 ++++++ ...test.ts => get_apm_agent_commands.test.ts} | 44 ++--- ..._commands.ts => get_apm_agent_commands.ts} | 6 +- .../config_agent/config_agent.stories.tsx | 1 + .../public/tutorial/config_agent/index.tsx | 22 +-- .../opentelemetry_instructions.tsx | 152 ++++++++++++++++++ .../apm/server/tutorial/envs/elastic_cloud.ts | 8 + .../apm/server/tutorial/envs/on_prem.ts | 5 + 14 files changed, 398 insertions(+), 63 deletions(-) create mode 100644 x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/tutorial/tutorial.spec.ts create mode 100644 x-pack/plugins/apm/public/tutorial/config_agent/agent_config_instructions.test.tsx create mode 100644 x-pack/plugins/apm/public/tutorial/config_agent/agent_config_instructions.tsx rename x-pack/plugins/apm/public/tutorial/config_agent/commands/{get_commands.test.ts => get_apm_agent_commands.test.ts} (94%) rename x-pack/plugins/apm/public/tutorial/config_agent/commands/{get_commands.ts => get_apm_agent_commands.ts} (86%) create mode 100644 x-pack/plugins/apm/public/tutorial/config_agent/opentelemetry_instructions.tsx diff --git a/src/plugins/home/common/instruction_variant.ts b/src/plugins/home/common/instruction_variant.ts index f19aa7849586b..d134d41fffa32 100644 --- a/src/plugins/home/common/instruction_variant.ts +++ b/src/plugins/home/common/instruction_variant.ts @@ -27,6 +27,7 @@ export const INSTRUCTION_VARIANT = { LINUX: 'linux', PHP: 'php', FLEET: 'fleet', + OPEN_TELEMETRY: 'openTelemetry', }; const DISPLAY_MAP = { @@ -50,6 +51,7 @@ const DISPLAY_MAP = { [INSTRUCTION_VARIANT.FLEET]: i18n.translate('home.tutorial.instruction_variant.fleet', { defaultMessage: 'Elastic APM in Fleet', }), + [INSTRUCTION_VARIANT.OPEN_TELEMETRY]: 'OpenTelemetry', }; /** diff --git a/x-pack/plugins/apm/common/tutorial/instructions/apm_agent_instructions.ts b/x-pack/plugins/apm/common/tutorial/instructions/apm_agent_instructions.ts index 695cf38bf2da2..b1c5fc79816ac 100644 --- a/x-pack/plugins/apm/common/tutorial/instructions/apm_agent_instructions.ts +++ b/x-pack/plugins/apm/common/tutorial/instructions/apm_agent_instructions.ts @@ -593,3 +593,46 @@ export const createPhpAgentInstructions = ( ), }, ]; + +export const createOpenTelemetryAgentInstructions = ( + apmServerUrl = '', + secretToken = '' +) => [ + { + title: i18n.translate('xpack.apm.tutorial.otel.download.title', { + defaultMessage: 'Download the OpenTelemetry APM Agent or SDK', + }), + textPre: i18n.translate('xpack.apm.tutorial.otel.download.textPre', { + defaultMessage: + 'See the [OpenTelemetry Instrumentation guides]({openTelemetryInstrumentationLink}) to download the OpenTelemetry Agent or SDK for your language.', + values: { + openTelemetryInstrumentationLink: + 'https://opentelemetry.io/docs/instrumentation', + }, + }), + }, + { + title: i18n.translate('xpack.apm.tutorial.otel.configureAgent.title', { + defaultMessage: 'Configure OpenTelemetry in your application', + }), + textPre: i18n.translate('xpack.apm.tutorial.otel.configureAgent.textPre', { + defaultMessage: + 'Specify the following OpenTelemetry settings as part of the startup of your application. Note that OpenTelemetry SDKs require some bootstrap code in addition to these configuration settings. For more details, see the [Elastic OpenTelemetry documentation]({openTelemetryDocumentationLink}) and the [OpenTelemetry community instrumentation guides]({openTelemetryInstrumentationLink}).', + values: { + openTelemetryDocumentationLink: + '{config.docs.base_url}guide/en/apm/guide/current/open-telemetry.html', + openTelemetryInstrumentationLink: + 'https://opentelemetry.io/docs/instrumentation', + }, + }), + customComponentName: 'TutorialConfigAgent', + textPost: i18n.translate('xpack.apm.tutorial.otel.configure.textPost', { + defaultMessage: + 'See the [documentation]({documentationLink}) for configuration options and advanced usage.\n\n', + values: { + documentationLink: + '{config.docs.base_url}guide/en/apm/guide/current/open-telemetry.html', + }, + }), + }, +]; diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/tutorial/tutorial.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/tutorial/tutorial.spec.ts new file mode 100644 index 0000000000000..e2ebe9c2bdda8 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/tutorial/tutorial.spec.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +describe('APM tutorial', () => { + before(() => { + cy.loginAsViewerUser(); + cy.visit('/app/home#/tutorial/apm'); + }); + + it('includes section for APM Server', () => { + cy.contains('APM Server'); + cy.contains('Linux DEB'); + cy.contains('Linux RPM'); + cy.contains('macOS'); + cy.contains('Windows'); + cy.contains('Fleet'); + }); + + it('includes section for APM Agents', () => { + cy.contains('APM agents'); + cy.contains('Java'); + cy.contains('RUM'); + cy.contains('Node.js'); + cy.contains('Django'); + cy.contains('Flask'); + cy.contains('Ruby on Rails'); + cy.contains('Rack'); + cy.contains('Go'); + cy.contains('.NET'); + cy.contains('PHP'); + }); +}); diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_accordion.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_accordion.tsx index a9ec9778ed3e6..3e4ddc36413c2 100644 --- a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_accordion.tsx +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_accordion.tsx @@ -29,7 +29,7 @@ import type { PackagePolicy, PackagePolicyEditExtensionComponentProps, } from '../apm_policy_form/typings'; -import { getCommands } from '../../../tutorial/config_agent/commands/get_commands'; +import { AgentConfigInstructions } from '../../../tutorial/config_agent/agent_config_instructions'; import { renderMustache } from './render_mustache'; import { TechnicalPreviewBadge } from '../../shared/technical_preview_badge'; @@ -83,26 +83,6 @@ function InstructionsContent({ markdown }: { markdown: string }) { ); } -function TutorialConfigAgent({ - variantId, - apmServerUrl, - secretToken, -}: { - variantId: string; - apmServerUrl?: string; - secretToken?: string; -}) { - const commandBlock = getCommands({ - variantId, - policyDetails: { apmServerUrl, secretToken }, - }); - return ( - - {commandBlock} - - ); -} - interface Props { policy: PackagePolicy; newPolicy: NewPackagePolicy; @@ -170,14 +150,14 @@ export function AgentInstructionsAccordion({ )} {customComponentName === 'TutorialConfigAgent' && ( - )} {customComponentName === 'TutorialConfigAgentRumScript' && ( - {children}; +} + +describe('AgentConfigInstructions', () => { + let getApmAgentCommandsSpy: jest.SpyInstance; + beforeAll(() => { + getApmAgentCommandsSpy = jest.spyOn(getCommands, 'getApmAgentCommands'); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it('renders OpenTelemetry instructions when the variant is "openTelemetry"', async () => { + const component = render( + , + { wrapper: Wrapper } + ); + + expect(getApmAgentCommandsSpy).not.toHaveBeenCalled(); + expect( + await component.queryByTestId('otel-instructions-table') + ).toBeInTheDocument(); + expect(await component.queryByTestId('commands')).not.toBeInTheDocument(); + }); + + it('calls getApmAgentCommands and renders the java instructions when the variant is "java"', async () => { + const component = render( + + ); + + expect(getApmAgentCommandsSpy).toHaveBeenCalled(); + expect(await component.queryByTestId('commands')).toBeInTheDocument(); + expect( + await component.queryByTestId('otel-instructions-table') + ).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/agent_config_instructions.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/agent_config_instructions.tsx new file mode 100644 index 0000000000000..5c5bb3282d217 --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/agent_config_instructions.tsx @@ -0,0 +1,50 @@ +/* + * 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 React from 'react'; +import { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; +import { OpenTelemetryInstructions } from './opentelemetry_instructions'; +import { getApmAgentCommands } from './commands/get_apm_agent_commands'; + +export function AgentConfigInstructions({ + variantId, + apmServerUrl, + secretToken, +}: { + variantId: string; + apmServerUrl?: string; + secretToken?: string; +}) { + if (variantId === 'openTelemetry') { + return ( + <> + + + + ); + } + + const commands = getApmAgentCommands({ + variantId, + policyDetails: { + apmServerUrl, + secretToken, + }, + }); + + return ( + <> + + + {commands} + + + ); +} diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_commands.test.ts b/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_apm_agent_commands.test.ts similarity index 94% rename from x-pack/plugins/apm/public/tutorial/config_agent/commands/get_commands.test.ts rename to x-pack/plugins/apm/public/tutorial/config_agent/commands/get_apm_agent_commands.test.ts index 7eddb706cb531..ba21a35768c42 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_commands.test.ts +++ b/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_apm_agent_commands.test.ts @@ -5,12 +5,12 @@ * 2.0. */ -import { getCommands } from './get_commands'; +import { getApmAgentCommands } from './get_apm_agent_commands'; describe('getCommands', () => { describe('unknown agent', () => { it('renders empty command', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'foo', policyDetails: { apmServerUrl: 'localhost:8220', @@ -22,7 +22,7 @@ describe('getCommands', () => { }); describe('java agent', () => { it('renders empty commands', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'java', policyDetails: {}, }); @@ -37,7 +37,7 @@ describe('getCommands', () => { `); }); it('renders with secret token and url', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'java', policyDetails: { apmServerUrl: 'localhost:8220', @@ -58,7 +58,7 @@ describe('getCommands', () => { }); describe('RUM(js) agent', () => { it('renders empty commands', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'js', policyDetails: {}, }); @@ -82,7 +82,7 @@ describe('getCommands', () => { `); }); it('renders with secret token and url', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'js', policyDetails: { apmServerUrl: 'localhost:8220', @@ -111,7 +111,7 @@ describe('getCommands', () => { }); describe('Node.js agent', () => { it('renders empty commands', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'node', policyDetails: {}, }); @@ -136,7 +136,7 @@ describe('getCommands', () => { `); }); it('renders with secret token and url', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'node', policyDetails: { apmServerUrl: 'localhost:8220', @@ -166,7 +166,7 @@ describe('getCommands', () => { }); describe('Django agent', () => { it('renders empty commands', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'django', policyDetails: {}, }); @@ -201,7 +201,7 @@ describe('getCommands', () => { `); }); it('renders with secret token and url', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'django', policyDetails: { apmServerUrl: 'localhost:8220', @@ -241,7 +241,7 @@ describe('getCommands', () => { }); describe('Flask agent', () => { it('renders empty commands', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'flask', policyDetails: {}, }); @@ -273,7 +273,7 @@ describe('getCommands', () => { `); }); it('renders with secret token and url', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'flask', policyDetails: { apmServerUrl: 'localhost:8220', @@ -310,7 +310,7 @@ describe('getCommands', () => { }); describe('Ruby on Rails agent', () => { it('renders empty commands', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'rails', policyDetails: {}, }); @@ -333,7 +333,7 @@ describe('getCommands', () => { `); }); it('renders with secret token and url', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'rails', policyDetails: { apmServerUrl: 'localhost:8220', @@ -361,7 +361,7 @@ describe('getCommands', () => { }); describe('Rack agent', () => { it('renders empty commands', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'rack', policyDetails: {}, }); @@ -384,7 +384,7 @@ describe('getCommands', () => { `); }); it('renders with secret token and url', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'rack', policyDetails: { apmServerUrl: 'localhost:8220', @@ -412,7 +412,7 @@ describe('getCommands', () => { }); describe('Go agent', () => { it('renders empty commands', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'go', policyDetails: {}, }); @@ -436,7 +436,7 @@ describe('getCommands', () => { `); }); it('renders with secret token and url', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'go', policyDetails: { apmServerUrl: 'localhost:8220', @@ -465,7 +465,7 @@ describe('getCommands', () => { }); describe('dotNet agent', () => { it('renders empty commands', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'dotnet', policyDetails: {}, }); @@ -482,7 +482,7 @@ describe('getCommands', () => { `); }); it('renders with secret token and url', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'dotnet', policyDetails: { apmServerUrl: 'localhost:8220', @@ -504,7 +504,7 @@ describe('getCommands', () => { }); describe('PHP agent', () => { it('renders empty commands', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'php', policyDetails: {}, }); @@ -517,7 +517,7 @@ describe('getCommands', () => { `); }); it('renders with secret token and url', () => { - const commands = getCommands({ + const commands = getApmAgentCommands({ variantId: 'php', policyDetails: { apmServerUrl: 'localhost:8220', diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_commands.ts b/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_apm_agent_commands.ts similarity index 86% rename from x-pack/plugins/apm/public/tutorial/config_agent/commands/get_commands.ts rename to x-pack/plugins/apm/public/tutorial/config_agent/commands/get_apm_agent_commands.ts index 73a388c3f735e..d217397f03f2e 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_commands.ts +++ b/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_apm_agent_commands.ts @@ -16,7 +16,7 @@ import { dotnet } from './dotnet'; import { php } from './php'; import { rum, rumScript } from './rum'; -const commandsMap: Record = { +const apmAgentCommandsMap: Record = { java, node, django, @@ -30,7 +30,7 @@ const commandsMap: Record = { js_script: rumScript, }; -export function getCommands({ +export function getApmAgentCommands({ variantId, policyDetails, }: { @@ -40,7 +40,7 @@ export function getCommands({ secretToken?: string; }; }) { - const commands = commandsMap[variantId]; + const commands = apmAgentCommandsMap[variantId]; if (!commands) { return ''; } diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/config_agent.stories.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/config_agent.stories.tsx index 2ef44b9538318..9ae7ba7f23c46 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/config_agent.stories.tsx +++ b/x-pack/plugins/apm/public/tutorial/config_agent/config_agent.stories.tsx @@ -107,6 +107,7 @@ export default { 'php', 'js', 'js_script', + 'openTelemetry', ], }, }, diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx index 72fb4f453eff8..cba037d8939d8 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx +++ b/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx @@ -4,13 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiCodeBlock, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { HttpStart } from '@kbn/core/public'; import React, { useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { APIReturnType } from '../../services/rest/create_call_apm_api'; -import { getCommands } from './commands/get_commands'; +import { AgentConfigInstructions } from './agent_config_instructions'; import { getPolicyOptions, PolicyOption } from './get_policy_options'; import { PolicySelector } from './policy_selector'; @@ -121,14 +121,6 @@ function TutorialConfigAgent({ ); } - const commands = getCommands({ - variantId, - policyDetails: { - apmServerUrl: selectedOption?.apmServerUrl, - secretToken: selectedOption?.secretToken, - }, - }); - const hasFleetAgents = !!data.fleetAgents.length; return ( @@ -144,11 +136,11 @@ function TutorialConfigAgent({ kibanaVersion, })} /> - - - - {commands} - + ); } diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/opentelemetry_instructions.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/opentelemetry_instructions.tsx new file mode 100644 index 0000000000000..bdb316643aa1c --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/opentelemetry_instructions.tsx @@ -0,0 +1,152 @@ +/* + * 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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiBasicTable, + EuiLink, + EuiSpacer, + EuiText, + EuiBasicTableColumn, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { ValuesType } from 'utility-types'; + +interface Props { + apmServerUrl?: string; + secretToken?: string; +} + +export function OpenTelemetryInstructions({ + apmServerUrl, + secretToken, +}: Props) { + const items = [ + { + setting: 'OTEL_EXPORTER_OTLP_ENDPOINT', + value: apmServerUrl ? apmServerUrl : '', + }, + { + setting: 'OTEL_EXPORTER_OTLP_HEADERS', + value: `"Authorization=Bearer ${ + secretToken ? secretToken : '' + }"`, + }, + { + setting: 'OTEL_METRICS_EXPORTER', + value: 'otlp', + notes: 'Enable metrics when supported by your OpenTelemtry client.', + }, + { + setting: 'OTEL_LOGS_EXPORTER', + value: 'otlp', + notes: 'Enable logs when supported by your OpenTelemetry client', + }, + { + setting: 'OTEL_RESOURCE_ATTRIBUTES', + value: + 'service.name=,service.version=,deployment.environment=production', + }, + ]; + + const columns: Array>> = [ + { + field: 'setting', + name: i18n.translate( + 'xpack.apm.tutorial.config_otel.column.configSettings', + { + defaultMessage: 'Configuration setting (1)', + } + ), + }, + { + field: 'value', + name: i18n.translate( + 'xpack.apm.tutorial.config_otel.column.configValue', + { + defaultMessage: 'Configuration value', + } + ), + render: (_, { value }) => {value}, + }, + { + field: 'notes', + name: i18n.translate('xpack.apm.tutorial.config_otel.column.notes', { + defaultMessage: 'Notes', + }), + }, + ]; + + return ( + <> + + + + + OTEL_EXPORTER_OTLP_ENDPOINT + + ), + otelExporterOtlpHeaders: ( + + OTEL_EXPORTER_OTLP_HEADERS + + ), + otelResourceAttributes: ( + + OTEL_RESOURCE_ATTRIBUTES + + ), + }} + /> + + + + + {i18n.translate( + 'xpack.apm.tutorial.config_otel.instrumentationGuide', + { + defaultMessage: 'OpenTelemetry Instrumentation guide', + } + )} + + ), + }} + /> + + + ); +} diff --git a/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts b/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts index ed486842bbf3a..f2804ce645a7f 100644 --- a/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts +++ b/x-pack/plugins/apm/server/tutorial/envs/elastic_cloud.ts @@ -24,6 +24,7 @@ import { createJavaAgentInstructions, createDotNetAgentInstructions, createPhpAgentInstructions, + createOpenTelemetryAgentInstructions, } from '../../../common/tutorial/instructions/apm_agent_instructions'; import { APMConfig } from '../..'; import { getOnPremApmServerInstructionSet } from './on_prem_apm_server_instruction_set'; @@ -132,6 +133,13 @@ function getApmAgentInstructionSet( id: INSTRUCTION_VARIANT.PHP, instructions: createPhpAgentInstructions(apmServerUrl, secretToken), }, + { + id: INSTRUCTION_VARIANT.OPEN_TELEMETRY, + instructions: createOpenTelemetryAgentInstructions( + apmServerUrl, + secretToken + ), + }, ], }; } diff --git a/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts index a89365b1b56da..1780c5bd87bcd 100644 --- a/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts +++ b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts @@ -22,6 +22,7 @@ import { createPhpAgentInstructions, createRackAgentInstructions, createRailsAgentInstructions, + createOpenTelemetryAgentInstructions, } from '../../../common/tutorial/instructions/apm_agent_instructions'; import { getOnPremApmServerInstructionSet } from './on_prem_apm_server_instruction_set'; @@ -80,6 +81,10 @@ export function onPremInstructions({ id: INSTRUCTION_VARIANT.PHP, instructions: createPhpAgentInstructions(), }, + { + id: INSTRUCTION_VARIANT.OPEN_TELEMETRY, + instructions: createOpenTelemetryAgentInstructions(), + }, ], statusCheck: { title: i18n.translate( From 64771d921f4458bbf888cea2a72ffaf700d50c9d Mon Sep 17 00:00:00 2001 From: Baturalp Gurdin <9674241+suchcodemuchwow@users.noreply.github.com> Date: Thu, 23 Jun 2022 19:03:31 +0200 Subject: [PATCH 42/54] correct apm server url (#135054) --- .../scripts/steps/functional/report_performance_metrics.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildkite/scripts/steps/functional/report_performance_metrics.sh b/.buildkite/scripts/steps/functional/report_performance_metrics.sh index 66a5ac27a8dff..da76ae1e0599b 100644 --- a/.buildkite/scripts/steps/functional/report_performance_metrics.sh +++ b/.buildkite/scripts/steps/functional/report_performance_metrics.sh @@ -7,7 +7,7 @@ source .buildkite/scripts/common/util.sh # TODO: Add new user and change lines accordingly USER_FROM_VAULT="$(retry 5 5 vault read -field=username secret/kibana-issues/dev/ci_stats_performance_metrics)" PASS_FROM_VAULT="$(retry 5 5 vault read -field=password secret/kibana-issues/dev/ci_stats_performance_metrics)" -APM_SERVER_URL="https://kibana-ops-e2e-perf.es.us-central1.gcp.cloud.es.io:9243/internal/apm" +APM_SERVER_URL="https://kibana-ops-e2e-perf.kb.us-central1.gcp.cloud.es.io:9243/internal/apm" BUILD_ID=${BUILDKITE_BUILD_ID} .buildkite/scripts/bootstrap.sh From 051dfe77f23a16637b6430d9a95776ca39a632be Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Thu, 23 Jun 2022 19:24:58 +0200 Subject: [PATCH 43/54] [Security Solution] Skips `Auto refreshes rules` failing test to unblock main (#135059) --- .../cypress/integration/detection_rules/sorting.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts index 63dcbf872d626..54c3e055e1efe 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts @@ -101,7 +101,8 @@ describe('Alerts detection rules', () => { .should('have.class', 'euiPaginationButton-isActive'); }); - it('Auto refreshes rules', () => { + // Refer to https://github.com/elastic/kibana/issues/135057 for the skipped test + it.skip('Auto refreshes rules', () => { /** * Ran into the error: timer created with setInterval() but cleared with cancelAnimationFrame() * There are no cancelAnimationFrames in the codebase that are used to clear a setInterval so From c6768782f32399b6c63fd23346210512504d5fc1 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 23 Jun 2022 10:25:49 -0700 Subject: [PATCH 44/54] [DOCS] Edits OAuth instructions in ServiceNow connector docs (#134970) --- .../action-types/servicenow-itom.asciidoc | 139 ++++------------- .../action-types/servicenow-sir.asciidoc | 147 +++++------------- .../action-types/servicenow.asciidoc | 80 ++++++---- docs/management/connectors/index.asciidoc | 6 +- 4 files changed, 122 insertions(+), 250 deletions(-) diff --git a/docs/management/connectors/action-types/servicenow-itom.asciidoc b/docs/management/connectors/action-types/servicenow-itom.asciidoc index 3fd3682dc1ad9..07ede3ef0d3cb 100644 --- a/docs/management/connectors/action-types/servicenow-itom.asciidoc +++ b/docs/management/connectors/action-types/servicenow-itom.asciidoc @@ -1,24 +1,25 @@ -[role="xpack"] [[servicenow-itom-action-type]] -=== ServiceNow ITOM connector and action +== {sn-itom} connector and action ++++ -ServiceNow ITOM +{sn-itom} ++++ -The {sn} ITOM connector uses the https://docs.servicenow.com/bundle/rome-it-operations-management/page/product/event-management/task/send-events-via-web-service.html[Event API] to create {sn} events. +The {sn-itom} connector uses the +https://docs.servicenow.com/bundle/rome-it-operations-management/page/product/event-management/task/send-events-via-web-service.html[event API] +to create {sn} events. [float] [[servicenow-itom-connector-prerequisites]] -==== Prerequisites -* Create a {sn} integration user and assign it the appropriate roles. +=== Prerequisites -If you use open authorization (OAuth), you must also: - -* Create an RSA keypair and add an X.509 Certificate. -* Create an OAuth JWT API endpoint for external clients with a JWT Verifiers Map. +. <> +. If you use open authorization (OAuth), you must also: +.. <>. +.. <>. [float] -===== Create a {sn} integration user +[[servicenow-itom-connector-prerequisites-integration-user]] +==== Create a {sn} integration user To ensure authenticated communication between Elastic and {sn}, create a {sn} integration user and assign it the appropriate roles. @@ -26,104 +27,32 @@ To ensure authenticated communication between Elastic and {sn}, create a {sn} in . Click *New*. . Complete the form, then right-click on the menu bar and click *Save*. . Go to the *Roles* tab and click *Edit*. -. Assign the integration user the following roles:  +. Assign the integration user the following roles: * `personalize_choices`: Allows the user to retrieve Choice element options, such as Severity. * `evt_mgmt_integration`: Enables integration with external event sources by allowing the user to create events. . Click *Save*. [float] -===== Create an RSA keypair and add an X.509 Certificate +[[servicenow-itom-connector-prerequisites-rsa-key]] +==== Create an RSA keypair and add an X.509 Certificate This step is required to use OAuth for authentication between Elastic and {sn}. -*Create an RSA keypair:* +include::servicenow.asciidoc[tag=servicenow-rsa-key] -. Use https://www.openssl.org/docs/man1.0.2/man1/genrsa.html[OpenSSL] to generate an RSA private key: -+ --- -[source,sh] ----- -openssl genrsa -out example-private-key.pem 3072 -openssl genrsa -passout pass:foobar -out example-private-key-with-password.pem 3072 <1> ----- -<1> Use the `passout` option to set a password on your private key. This is optional but remember your password if you set one. --- - -. Use https://www.openssl.org/docs/man1.0.2/man1/req.html[OpenSSL] to generate the matching public key: -+ --- -[source,sh] ----- -openssl req -new -x509 -key example-private-key.pem -out example-sn-cert.pem -days 360 ----- --- - -*Add an X.509 Certificate to ServiceNow:* - -. In your {sn} instance, go to *Certificates* and select *New*. -. Configure the certificate as follows: -+ --- -* *Name*: Name the certificate. -* *PEM Certificate*: Copy the generated public key into this text field. - -[role="screenshot"] -image::management/connectors/images/servicenow-new-certificate.png[Shows new certificate form in ServiceNow] --- - -. Click *Submit* to create the certificate. +include::servicenow.asciidoc[tag=servicenow-certificate] [float] -===== Create an OAuth JWT API endpoint for external clients with a JWT Verifiers Map +[[servicenow-itom-connector-prerequisites-endpoint]] +==== Create an OAuth JWT API endpoint for external clients with a JWT Verifiers Map -This step is required to use OAuth for authentication between Elastic and {sn}. - -. In your {sn} instance, go to *Application Registry* and select *New*. -. Select *Create an OAuth JWT API endpoint for external clients* from the list of options. -+ --- -[role="screenshot"] -image::management/connectors/images/servicenow-jwt-endpoint.png[Shows application type selection] --- - -. Configure the application as follows: -+ --- -* *Name*: Name the application. -* *User field*: Select the field to use as the user identifier. - -[role="screenshot"] -image::management/connectors/images/servicenow-new-application.png[Shows new application form in ServiceNow] - -IMPORTANT: Remember the selected user field. You will use this as the *User Identifier Value* when creating the connector. For example, if you selected *Email* for *User field*, you will use the user's email for the *User Identifier Value*. --- - -. Click *Submit* to create the application. You will be redirected to the list of applications. -. Select the application you just created. -. Find the *Jwt Verifier Maps* tab and click *New*. -. Configure the new record as follows: -+ --- -* *Name*: Name the JWT Verifier Map. -* *Sys certificate*: Click the search icon and select the name of the certificate created in the previous step. - -[role="screenshot"] -image::management/connectors/images/servicenow-new-jwt-verifier-map.png[Shows new JWT Verifier Map form in ServiceNow] --- - -. Click *Submit* to create the application. -. Note the *Client ID*, *Client Secret* and *JWT Key ID*. You will need these values to create your {sn} connector. -+ --- -[role="screenshot"] -image::management/connectors/images/servicenow-oauth-values.png[Shows where to find OAuth values in ServiceNow] --- +include::servicenow.asciidoc[tag=servicenow-endpoint] [float] [[servicenow-itom-connector-configuration]] -==== Connector configuration +=== Connector configuration -{sn} ITOM connectors have the following configuration properties. +{sn-itom} connectors have the following configuration properties. Name:: The name of the connector. The name is used to identify a connector in the **Stack Management** connector listing, and in the connector list when configuring an action. Is OAuth:: The type of authentication to use. @@ -139,13 +68,13 @@ Private Key Password:: The password for the RSA private key generated during set [float] [[servicenow-itom-connector-networking-configuration]] -==== Connector networking configuration +=== Connector networking configuration Use the <> to customize connector networking configurations, such as proxies, certificates, or TLS settings. You can set configurations that apply to all your connectors or use `xpack.actions.customHostSettings` to set per-host configurations. [float] [[Preconfigured-servicenow-itom-configuration]] -==== Preconfigured connector type +=== Preconfigured connector type Connector using Basic Authentication [source,text] @@ -196,26 +125,26 @@ Secrets defines sensitive information for the connector type. [float] [[define-servicenow-itom-ui]] -==== Define connector in Stack Management +=== Define connector in Stack Management -Define {sn} ITOM connector properties. Choose whether to use OAuth for authentication. +Define {sn-itom} connector properties. Choose whether to use OAuth for authentication. [role="screenshot"] -image::management/connectors/images/servicenow-itom-connector-basic.png[ServiceNow ITOM connector using basic auth] +image::management/connectors/images/servicenow-itom-connector-basic.png[{sn-itom} connector using basic auth] [role="screenshot"] -image::management/connectors/images/servicenow-itom-connector-oauth.png[ServiceNow ITOM connector using OAuth] +image::management/connectors/images/servicenow-itom-connector-oauth.png[{sn-itom} connector using OAuth] -Test {sn} ITOM action parameters. +Test {sn-itom} action parameters. [role="screenshot"] -image::management/connectors/images/servicenow-itom-params-test.png[ServiceNow ITOM params test] +image::management/connectors/images/servicenow-itom-params-test.png[{sn-itom} params test] [float] [[servicenow-itom-action-configuration]] -==== Action configuration +=== Action configuration -{sn} ITOM actions have the following configuration properties. +{sn-itom} actions have the following configuration properties. Source:: The name of the event source type. Node:: The Host that the event was triggered for. @@ -227,10 +156,10 @@ Message key:: All actions sharing this key will be associated with the same {sn Severity:: The severity of the event. Description:: The details about the event. -Refer to https://docs.servicenow.com/bundle/rome-it-operations-management/page/product/event-management/task/send-events-via-web-service.html[ServiceNow documentation] for more information about the properties. +Refer to https://docs.servicenow.com/bundle/rome-it-operations-management/page/product/event-management/task/send-events-via-web-service.html[{sn} documentation] for more information about the properties. [float] [[configuring-servicenow-itom]] -==== Configure {sn} ITOM +=== Configure {sn-itom} {sn} offers free https://developer.servicenow.com/dev.do#!/guides/madrid/now-platform/pdi-guide/obtaining-a-pdi[Personal Developer Instances], which you can use to test incidents. diff --git a/docs/management/connectors/action-types/servicenow-sir.asciidoc b/docs/management/connectors/action-types/servicenow-sir.asciidoc index a3618d626d8be..06639a077bf89 100644 --- a/docs/management/connectors/action-types/servicenow-sir.asciidoc +++ b/docs/management/connectors/action-types/servicenow-sir.asciidoc @@ -1,28 +1,28 @@ -[role="xpack"] [[servicenow-sir-action-type]] -=== ServiceNow SecOps connector and action +== {sn-sir} connector and action ++++ -ServiceNow SecOps +{sn-sir} ++++ -The {sn} SecOps connector uses the https://developer.servicenow.com/dev.do#!/reference/api/sandiego/rest/c_ImportSetAPI[Import Set API] to create {sn} security incidents. +The {sn-sir} connector uses the +https://developer.servicenow.com/dev.do#!/reference/api/sandiego/rest/c_ImportSetAPI[import set API] +to create {sn} security incidents. [float] [[servicenow-sir-connector-prerequisites]] -==== Prerequisites -After upgrading from {stack} version 7.15.0 or earlier to version 7.16.0 or later, you must complete the following within your {sn} instance before creating a new {sn} SecOps connector or <>: +=== Prerequisites +After upgrading from {stack} version 7.15.0 or earlier to version 7.16.0 or later, you must complete the following within your {sn} instance before creating a new {sn-sir} connector or <>: -* Install https://store.servicenow.com/sn_appstore_store.do#!/store/application/2f0746801baeb01019ae54e4604bcb0f[Elastic for Security Operations (SecOps)] from the {sn} Store. -* Create a {sn} integration user and assign it the appropriate roles. -* Create a Cross-Origin Resource Sharing (CORS) rule. - -If you use open authorization (OAuth), you must also: - -* Create an RSA keypair and add an X.509 Certificate. -* Create an OAuth JWT API endpoint for external clients with a JWT Verifiers Map. +. Install https://store.servicenow.com/sn_appstore_store.do#!/store/application/2f0746801baeb01019ae54e4604bcb0f[Elastic for Security Operations (SecOps)] from the {sn} Store. +. <>. +. <>. +. If you use open authorization (OAuth), you must also: +.. <>. +.. <>. [float] -===== Create a {sn} integration user +[[servicenow-sir-connector-prerequisites-integration-user]] +==== Create a {sn} integration user To ensure authenticated communication between Elastic and {sn}, create a {sn} integration user and assign it the appropriate roles.  @@ -39,7 +39,8 @@ To ensure authenticated communication between Elastic and {sn}, create a {sn} in . Click *Save*. [float] -===== Create a CORS rule +[[servicenow-sir-connector-prerequisites-cors-rule]] +==== Create a CORS rule A CORS rule is required for communication between Elastic and {sn}. To create a CORS rule: @@ -53,98 +54,26 @@ A CORS rule is required for communication between Elastic and {sn}. To create a . Click *Submit* to create the rule. [float] -===== Create an RSA keypair and add an X.509 Certificate +[[servicenow-sir-connector-prerequisites-rsa-key]] +==== Create an RSA keypair and add an X.509 Certificate This step is required to use OAuth for authentication between Elastic and {sn}. -*Create an RSA keypair:* +include::servicenow.asciidoc[tag=servicenow-rsa-key] -. Use https://www.openssl.org/docs/man1.0.2/man1/genrsa.html[OpenSSL] to generate an RSA private key: -+ --- -[source,sh] ----- -openssl genrsa -out example-private-key.pem 3072 -openssl genrsa -passout pass:foobar -out example-private-key-with-password.pem 3072 <1> ----- -<1> Use the `passout` option to set a password on your private key. This is optional but remember your password if you set one. --- - -. Use https://www.openssl.org/docs/man1.0.2/man1/req.html[OpenSSL] to generate the matching public key: -+ --- -[source,sh] ----- -openssl req -new -x509 -key example-private-key.pem -out example-sn-cert.pem -days 360 ----- --- - -*Add an X.509 Certificate to ServiceNow:* - -. In your {sn} instance, go to *Certificates* and select *New*. -. Configure the certificate as follows: -+ --- -* *Name*: Name the certificate. -* *PEM Certificate*: Copy the generated public key into this text field. - -[role="screenshot"] -image::management/connectors/images/servicenow-new-certificate.png[Shows new certificate form in ServiceNow] --- - -. Click *Submit* to create the certificate. +include::servicenow.asciidoc[tag=servicenow-certificate] [float] -===== Create an OAuth JWT API endpoint for external clients with a JWT Verifiers Map - -This step is required to use OAuth for authentication between Elastic and {sn}. - -. In your {sn} instance, go to *Application Registry* and select *New*. -. Select *Create an OAuth JWT API endpoint for external clients* from the list of options. -+ --- -[role="screenshot"] -image::management/connectors/images/servicenow-jwt-endpoint.png[Shows application type selection] --- - -. Configure the application as follows: -+ --- -* *Name*: Name the application. -* *User field*: Select the field to use as the user identifier. - -[role="screenshot"] -image::management/connectors/images/servicenow-new-application.png[Shows new application form in ServiceNow] - -IMPORTANT: Remember the selected user field. You will use this as the *User Identifier Value* when creating the connector. For example, if you selected *Email* for *User field*, you will use the user's email for the *User Identifier Value*. --- +[[servicenow-sir-connector-prerequisites-endpoint]] +==== Create an OAuth JWT API endpoint for external clients with a JWT Verifiers Map -. Click *Submit* to create the application. You will be redirected to the list of applications. -. Select the application you just created. -. Find the *Jwt Verifier Maps* tab and click *New*. -. Configure the new record as follows: -+ --- -* *Name*: Name the JWT Verifier Map. -* *Sys certificate*: Click the search icon and select the name of the certificate created in the previous step. - -[role="screenshot"] -image::management/connectors/images/servicenow-new-jwt-verifier-map.png[Shows new JWT Verifier Map form in ServiceNow] --- - -. Click *Submit* to create the verifier map. -. Note the *Client ID*, *Client Secret* and *JWT Key ID*. You will need these values to create your {sn} connector. -+ --- -[role="screenshot"] -image::management/connectors/images/servicenow-oauth-values.png[Shows where to find OAuth values in ServiceNow] --- +include::servicenow.asciidoc[tag=servicenow-endpoint] [float] [[servicenow-sir-connector-update]] -==== Update a deprecated {sn} SecOps connector +=== Update a deprecated {sn-sir} connector -{sn} SecOps connectors created in {stack} version 7.15.0 or earlier are marked as deprecated after you upgrade to version 7.16.0 or later. Deprecated connectors have a yellow icon after their name and display a warning message when selected. +{sn-sir} connectors created in {stack} version 7.15.0 or earlier are marked as deprecated after you upgrade to version 7.16.0 or later. Deprecated connectors have a yellow icon after their name and display a warning message when selected. [role="screenshot"] image::management/connectors/images/servicenow-sir-update-connector.png[Shows deprecated ServiceNow connectors] @@ -164,9 +93,9 @@ To update a deprecated connector: [float] [[servicenow-sir-connector-configuration]] -==== Connector configuration +=== Connector configuration -{sn} SecOps connectors have the following configuration properties. +{sn-sir} connectors have the following configuration properties. Name:: The name of the connector. The name is used to identify a connector in the **Stack Management** UI connector listing, and in the connector list when configuring an action. Is OAuth:: The type of authentication to use. @@ -182,13 +111,13 @@ Private Key Password:: The password for the RSA private key generated during set [float] [[servicenow-sir-connector-networking-configuration]] -==== Connector networking configuration +=== Connector networking configuration Use the <> to customize connector networking configurations, such as proxies, certificates, or TLS settings. You can set configurations that apply to all your connectors or use `xpack.actions.customHostSettings` to set per-host configurations. [float] [[Preconfigured-servicenow-sir-configuration]] -==== Preconfigured connector type +=== Preconfigured connector type Connector using Basic Authentication [source,text] @@ -244,26 +173,26 @@ Secrets defines sensitive information for the connector type. [float] [[define-servicenow-sir-ui]] -==== Define connector in Stack Management +=== Define connector in Stack Management Define {sn} SecOps connector properties. Choose whether to use OAuth for authentication. [role="screenshot"] -image::management/connectors/images/servicenow-sir-connector-basic.png[ServiceNow SecOps connector using basic auth] +image::management/connectors/images/servicenow-sir-connector-basic.png[{sn-sir} connector using basic auth] [role="screenshot"] -image::management/connectors/images/servicenow-sir-connector-oauth.png[ServiceNow SecOps connector using OAuth] +image::management/connectors/images/servicenow-sir-connector-oauth.png[{sn-sir} connector using OAuth] -Test {sn} SecOps action parameters. +Test {sn-sir} action parameters. [role="screenshot"] -image::management/connectors/images/servicenow-sir-params-test.png[ServiceNow SecOps params test] +image::management/connectors/images/servicenow-sir-params-test.png[{sn-sir} params test] [float] [[servicenow-sir-action-configuration]] -==== Action configuration +=== Action configuration -ServiceNow SecOps actions have the following configuration properties. +{sn-sir} actions have the following configuration properties. Short description:: A short description for the incident, used for searching the contents of the knowledge base. Priority:: The priority of the incident. @@ -279,6 +208,6 @@ Additional comments:: Additional information for the client, such as how to tro [float] [[configuring-servicenow-sir]] -==== Configure {sn} SecOps +=== Configure {sn-sir} {sn} offers free https://developer.servicenow.com/dev.do#!/guides/madrid/now-platform/pdi-guide/obtaining-a-pdi[Personal Developer Instances], which you can use to test incidents. diff --git a/docs/management/connectors/action-types/servicenow.asciidoc b/docs/management/connectors/action-types/servicenow.asciidoc index 99ed4f0bec32f..613935a7ac4d0 100644 --- a/docs/management/connectors/action-types/servicenow.asciidoc +++ b/docs/management/connectors/action-types/servicenow.asciidoc @@ -1,28 +1,34 @@ -[role="xpack"] [[servicenow-action-type]] -=== ServiceNow ITSM connector and action +== {sn-itsm} connector and action ++++ -ServiceNow ITSM +{sn-itsm} ++++ -The {sn} ITSM connector uses the https://developer.servicenow.com/dev.do#!/reference/api/sandiego/rest/c_ImportSetAPI[Import Set API] to create {sn} incidents. +The {sn-itsm} connector uses the +https://developer.servicenow.com/dev.do#!/reference/api/sandiego/rest/c_ImportSetAPI[import set API] +to create {sn} incidents. [float] [[servicenow-itsm-connector-prerequisites]] -==== Prerequisites -After upgrading from {stack} version 7.15.0 or earlier to version 7.16.0 or later, you must complete the following within your {sn} instance before creating a new {sn} ITSM connector or <>: - -* Install https://store.servicenow.com/sn_appstore_store.do#!/store/application/7148dbc91bf1f450ced060a7234bcb88[Elastic for ITSM] from the {sn} Store. -* Create a {sn} integration user and assign it the appropriate roles. -* Create a Cross-Origin Resource Sharing (CORS) rule. - -If you use open authorization (OAuth), you must also: - -* Create an RSA keypair and add an X.509 Certificate. -* Create an OAuth JWT API endpoint for external clients with a JWT Verifiers Map. +=== Prerequisites + +After upgrading from {stack} version 7.15.0 or earlier to version 7.16.0 or +later, you must complete the following steps within your {sn} instance before +creating a new {sn-itsm} connector or +<>: + +. Install +https://store.servicenow.com/sn_appstore_store.do#!/store/application/7148dbc91bf1f450ced060a7234bcb88[Elastic for ITSM] +from the {sn} Store. +. <>. +. <>. +. If you use open authorization (OAuth), you must also: +.. <>. +.. <>. [float] -===== Create a {sn} integration user +[[servicenow-itsm-connector-prerequisites-integration-user]] +==== Create a {sn} integration user To ensure authenticated communication between Elastic and {sn}, create a {sn} integration user and assign it the appropriate roles. @@ -38,7 +44,8 @@ To ensure authenticated communication between Elastic and {sn}, create a {sn} in . Click *Save*. [float] -===== Create a CORS rule +[[servicenow-itsm-connector-prerequisites-cors-rule]] +==== Create a CORS rule A CORS rule is required for communication between Elastic and {sn}. To create a CORS rule: @@ -52,10 +59,12 @@ A CORS rule is required for communication between Elastic and {sn}. To create a . Click *Submit* to create the rule. [float] -===== Create an RSA keypair and add an X.509 Certificate +[[servicenow-itsm-connector-prerequisites-rsa-key]] +==== Create an RSA keypair and add an X.509 certificate This step is required to use OAuth for authentication between Elastic and {sn}. +// tag::servicenow-rsa-key[] *Create an RSA keypair:* . Use https://www.openssl.org/docs/man1.0.2/man1/genrsa.html[OpenSSL] to generate an RSA private key: @@ -77,8 +86,9 @@ openssl genrsa -passout pass:foobar -out example-private-key-with-password.pem 3 openssl req -new -x509 -key example-private-key.pem -out example-sn-cert.pem -days 360 ---- -- - -*Add an X.509 Certificate to ServiceNow:* +// end::servicenow-rsa-key[] +// tag::servicenow-certificate[] +*Add an X.509 certificate to ServiceNow:* . In your {sn} instance, go to *Certificates* and select *New*. . Configure the certificate as follows: @@ -92,10 +102,13 @@ image::management/connectors/images/servicenow-new-certificate.png[Shows new cer -- . Click *Submit* to create the certificate. +// end::servicenow-certificate[] [float] -===== Create an OAuth JWT API endpoint for external clients with a JWT Verifiers Map +[[servicenow-itsm-connector-prerequisites-endpoint]] +==== Create an OAuth JWT API endpoint for external clients with a JWT Verifiers Map +// tag::servicenow-endpoint[] This step is required to use OAuth for authentication between Elastic and {sn}. . In your {sn} instance, go to *Application Registry* and select *New*. @@ -138,12 +151,13 @@ image::management/connectors/images/servicenow-new-jwt-verifier-map.png[Shows ne [role="screenshot"] image::management/connectors/images/servicenow-oauth-values.png[Shows where to find OAuth values in ServiceNow] -- +// end::servicenow-endpoint[] [float] [[servicenow-itsm-connector-update]] -==== Update a deprecated {sn} ITSM connector +=== Update a deprecated {sn-itsm} connector -{sn} ITSM connectors created in {stack} version 7.15.0 or earlier are marked as deprecated after you upgrade to version 7.16.0 or later. Deprecated connectors have a yellow icon after their name and display a warning message when selected. +{sn-itsm} connectors created in {stack} version 7.15.0 or earlier are marked as deprecated after you upgrade to version 7.16.0 or later. Deprecated connectors have a yellow icon after their name and display a warning message when selected. [role="screenshot"] image::management/connectors/images/servicenow-sir-update-connector.png[Shows deprecated ServiceNow connectors] @@ -163,9 +177,9 @@ To update a deprecated connector: [float] [[servicenow-connector-configuration]] -==== Connector configuration +=== Connector configuration -{sn} ITSM connectors have the following configuration properties. +{sn-itsm} connectors have the following configuration properties. Name:: The name of the connector. The name is used to identify a connector in the **Stack Management** UI connector listing, and in the connector list when configuring an action. Is OAuth:: The type of authentication to use. @@ -181,13 +195,13 @@ Private Key Password:: The password for the RSA private key generated during set [float] [[servicenow-connector-networking-configuration]] -==== Connector networking configuration +=== Connector networking configuration Use the <> to customize connector networking configurations, such as proxies, certificates, or TLS settings. You can set configurations that apply to all your connectors or use `xpack.actions.customHostSettings` to set per-host configurations. [float] [[Preconfigured-servicenow-configuration]] -==== Preconfigured connector type +=== Preconfigured connector type Connector using Basic Authentication [source,text] @@ -243,9 +257,9 @@ Secrets defines sensitive information for the connector type. [float] [[define-servicenow-ui]] -==== Define connector in Stack Management +=== Define connector in Stack Management -Define {sn} ITSM connector properties. Choose whether to use OAuth for authentication. +Define {sn-itsm} connector properties. Choose whether to use OAuth for authentication. [role="screenshot"] image::management/connectors/images/servicenow-connector-basic.png[ServiceNow connector using basic auth] @@ -253,16 +267,16 @@ image::management/connectors/images/servicenow-connector-basic.png[ServiceNow co [role="screenshot"] image::management/connectors/images/servicenow-connector-oauth.png[ServiceNow connector using OAuth] -Test {sn} ITSM action parameters. +Test {sn-itsm} action parameters. [role="screenshot"] image::management/connectors/images/servicenow-params-test.png[ServiceNow params test] [float] [[servicenow-action-configuration]] -==== Action configuration +=== Action configuration -{sn} ITSM actions have the following configuration properties. +{sn-itsm} actions have the following configuration properties. Urgency:: The extent to which the incident resolution can delay. Severity:: The severity of the incident. @@ -280,6 +294,6 @@ Additional comments:: Additional information for the client, such as how to tro [float] [[configuring-servicenow]] -==== Configure {sn} +=== Configure {sn} {sn} offers free https://developer.servicenow.com/dev.do#!/guides/madrid/now-platform/pdi-guide/obtaining-a-pdi[Personal Developer Instances], which you can use to test incidents. diff --git a/docs/management/connectors/index.asciidoc b/docs/management/connectors/index.asciidoc index c895c4450aace..322f248e4ca8c 100644 --- a/docs/management/connectors/index.asciidoc +++ b/docs/management/connectors/index.asciidoc @@ -5,9 +5,9 @@ include::action-types/jira.asciidoc[] include::action-types/teams.asciidoc[] include::action-types/pagerduty.asciidoc[] include::action-types/server-log.asciidoc[] -include::action-types/servicenow.asciidoc[] -include::action-types/servicenow-sir.asciidoc[] -include::action-types/servicenow-itom.asciidoc[] +include::action-types/servicenow.asciidoc[leveloffset=+1] +include::action-types/servicenow-sir.asciidoc[leveloffset=+1] +include::action-types/servicenow-itom.asciidoc[leveloffset=+1] include::action-types/swimlane.asciidoc[] include::action-types/slack.asciidoc[] include::action-types/webhook.asciidoc[] From 5989f1df496edb47edcaef7948d633fd5548bd06 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Thu, 23 Jun 2022 13:47:22 -0400 Subject: [PATCH 45/54] synthetics - adjust enabled key for browser monitors (#135017) --- .../synthetics_service/normalizers/browser.ts | 2 +- .../apis/uptime/rest/add_monitor_project.ts | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/synthetics/server/synthetics_service/normalizers/browser.ts b/x-pack/plugins/synthetics/server/synthetics_service/normalizers/browser.ts index 92d598192844b..207945d4e95d1 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/normalizers/browser.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/normalizers/browser.ts @@ -101,7 +101,7 @@ export const normalizeProjectMonitor = ({ [ConfigKey.ORIGINAL_SPACE]: namespace || defaultFields[ConfigKey.ORIGINAL_SPACE], [ConfigKey.CUSTOM_HEARTBEAT_ID]: `${monitor.id}-${projectId}-${namespace}`, [ConfigKey.TIMEOUT]: null, - [ConfigKey.ENABLED]: monitor.enabled || defaultFields[ConfigKey.ENABLED], + [ConfigKey.ENABLED]: monitor.enabled ?? defaultFields[ConfigKey.ENABLED], }; return { ...DEFAULT_FIELDS[DataStream.BROWSER], diff --git a/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts b/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts index 0445843ddd02e..aae7672f46344 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts @@ -629,5 +629,43 @@ export default function ({ getService }: FtrProviderContext) { ]); } }); + + it('project monitors - is able to enable and disable monitors', async () => { + try { + await supertest + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .set('kbn-xsrf', 'true') + .send(projectMonitors); + + await supertest + .put(API_URLS.SYNTHETICS_MONITORS_PROJECT) + .set('kbn-xsrf', 'true') + .send({ + ...projectMonitors, + monitors: [ + { + ...projectMonitors.monitors[0], + enabled: false, + }, + ], + }) + .expect(200); + const response = await supertest + .get(API_URLS.SYNTHETICS_MONITORS) + .query({ + filter: `${syntheticsMonitorType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, + }) + .set('kbn-xsrf', 'true') + .expect(200); + const { monitors } = response.body; + expect(monitors[0].attributes.enabled).eql(false); + } finally { + await Promise.all([ + projectMonitors.monitors.map((monitor) => { + return deleteMonitor(monitor.id, projectMonitors.project); + }), + ]); + } + }); }); } From 6aa6a50e30bc8d824ae54134afd22ec5dab004f3 Mon Sep 17 00:00:00 2001 From: Faisal Kanout Date: Thu, 23 Jun 2022 20:50:08 +0300 Subject: [PATCH 46/54] [Actionable Observability] - Rule Details Page - Update tag component to show tags out of the popover as an option (#134468) * Add spread prop to tag * Use spread prop in Rule Details page * Add unit test for spread prop * Update tag props type * Fix type guard * use generic type to make code cleaner * Updating var name * Update data test obj * [Code Review] Fix lazy import Co-authored-by: Xavier Mouligneau --- .../rule_details/components/page_title.tsx | 26 +++++------- .../public/pages/rules/index.tsx | 2 +- .../public/application/sections/index.tsx | 6 +-- .../components/rule_tag_badge.test.tsx | 8 ++++ .../rules_list/components/rule_tag_badge.tsx | 42 +++++++++++++++---- .../public/common/get_rule_tag_badge.tsx | 10 +++-- .../triggers_actions_ui/public/mocks.ts | 6 ++- .../triggers_actions_ui/public/plugin.ts | 7 +++- .../triggers_actions_ui/public/types.ts | 6 ++- 9 files changed, 76 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/page_title.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/page_title.tsx index 3901c13cd26a4..b44eb99e44a3d 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/components/page_title.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/components/page_title.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useState } from 'react'; +import React from 'react'; import moment from 'moment'; import { EuiText, EuiFlexGroup, EuiFlexItem, EuiBadge, EuiSpacer } from '@elastic/eui'; import { PageHeaderProps } from '../types'; @@ -14,12 +14,7 @@ import { getHealthColor } from '../../rules/config'; export function PageTitle({ rule }: PageHeaderProps) { const { triggersActionsUi } = useKibana().services; - const [isTagsPopoverOpen, setIsTagsPopoverOpen] = useState(false); - const tagsClicked = () => - setIsTagsPopoverOpen( - (oldStateIsTagsPopoverOpen) => rule.tags.length > 0 && !oldStateIsTagsPopoverOpen - ); - const closeTagsPopover = () => setIsTagsPopoverOpen(false); + return ( <> @@ -37,7 +32,7 @@ export function PageTitle({ rule }: PageHeaderProps) { - + {LAST_UPDATED_MESSAGE} {BY_WORD} {rule.updatedBy} {ON_WORD}  @@ -46,15 +41,14 @@ export function PageTitle({ rule }: PageHeaderProps) { {moment(rule.createdAt).format('ll')} - - {rule.tags.length > 0 && - triggersActionsUi.getRuleTagBadge({ - isOpen: isTagsPopoverOpen, - tags: rule.tags, - onClick: () => tagsClicked(), - onClose: () => closeTagsPopover(), - })} + + {rule.tags.length > 0 && + triggersActionsUi.getRuleTagBadge<'tagsOutPopover'>({ + tagsOutPopover: true, + tags: rule.tags, + })} + ); } diff --git a/x-pack/plugins/observability/public/pages/rules/index.tsx b/x-pack/plugins/observability/public/pages/rules/index.tsx index f4d6a3c10eeb0..43d98bb1b91b8 100644 --- a/x-pack/plugins/observability/public/pages/rules/index.tsx +++ b/x-pack/plugins/observability/public/pages/rules/index.tsx @@ -199,7 +199,7 @@ function RulesPage() { 'data-test-subj': 'rulesTableCell-tagsPopover', render: (ruleTags: string[], item: RuleTableItem) => { return ruleTags.length > 0 - ? triggersActionsUi.getRuleTagBadge({ + ? triggersActionsUi.getRuleTagBadge<'default'>({ isOpen: tagPopoverOpenIndex === item.index, tags: ruleTags, onClick: () => setTagPopoverOpenIndex(item.index), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx index 13adb84d5039b..4a64ebba43f68 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx @@ -40,9 +40,6 @@ export const RuleTagFilter = suspendedComponentWithProps( export const RuleStatusFilter = suspendedComponentWithProps( lazy(() => import('./rules_list/components/rule_status_filter')) ); -export const RuleTagBadge = suspendedComponentWithProps( - lazy(() => import('./rules_list/components/rule_tag_badge')) -); export const RuleEventLogList = suspendedComponentWithProps( lazy(() => import('./rule_details/components/rule_event_log_list')) ); @@ -52,3 +49,6 @@ export const RulesList = suspendedComponentWithProps( export const RulesListNotifyBadge = suspendedComponentWithProps( lazy(() => import('./rules_list/components/rules_list_notify_badge')) ); +export const RuleTagBadge = suspendedComponentWithProps( + lazy(() => import('./rules_list/components/rule_tag_badge')) +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_badge.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_badge.test.tsx index 606d60ff6bfeb..a1d3b20206dbd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_badge.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_badge.test.tsx @@ -56,4 +56,12 @@ describe('RuleTagBadge', () => { expect(onClickMock).toHaveBeenCalledTimes(2); }); + + it('shows all the tags without clicking when passing "spread" props with "true"', () => { + const wrapper = mountWithIntl(); + expect(wrapper.find('[data-test-subj="tagsOutPopover"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="ruleTagBadgeItem-a"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="ruleTagBadgeItem-b"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="ruleTagBadgeItem-c"]').exists()).toBeTruthy(); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_badge.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_badge.tsx index c7da398d14403..4c968dde75811 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_badge.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_badge.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiPopover, EuiBadge, EuiPopoverTitle } from '@elastic/eui'; +import { EuiPopover, EuiBadge, EuiPopoverTitle, EuiFlexGroup } from '@elastic/eui'; const tagTitle = i18n.translate( 'xpack.triggersActionsUI.sections.rules_list.rules_tag_badge.tagTitle', @@ -16,32 +16,43 @@ const tagTitle = i18n.translate( } ); -export interface RuleTagBadgeProps { +export type RuleTagBadgeOptions = 'tagsOutPopover' | 'default'; + +export interface RuleTagBadgeBasicOptions { isOpen: boolean; - tags: string[]; onClick: React.MouseEventHandler; onClose: () => void; +} + +export interface RuleTagBadgeCommonProps { + tagsOutPopover?: boolean; + tags: string[]; badgeDataTestSubj?: string; titleDataTestSubj?: string; tagItemDataTestSubj?: (tag: string) => string; } +export type RuleTagBadgeProps = T extends 'default' + ? RuleTagBadgeBasicOptions & RuleTagBadgeCommonProps + : T extends 'tagsOutPopover' + ? RuleTagBadgeCommonProps + : never; + const containerStyle = { width: '300px', }; const getTagItemDataTestSubj = (tag: string) => `ruleTagBadgeItem-${tag}`; -export const RuleTagBadge = (props: RuleTagBadgeProps) => { +export const RuleTagBadge = (props: RuleTagBadgeProps) => { const { - isOpen = false, + tagsOutPopover = false, tags = [], - onClick, - onClose, badgeDataTestSubj = 'ruleTagBadge', titleDataTestSubj = 'ruleTagPopoverTitle', tagItemDataTestSubj = getTagItemDataTestSubj, } = props; + const { isOpen, onClose, onClick } = props as RuleTagBadgeBasicOptions; const badge = useMemo(() => { return ( @@ -59,7 +70,7 @@ export const RuleTagBadge = (props: RuleTagBadgeProps) => { {tags.length} ); - }, [tags, badgeDataTestSubj, onClick]); + }, [badgeDataTestSubj, onClick, tags.length]); const tagBadges = useMemo( () => @@ -76,9 +87,22 @@ export const RuleTagBadge = (props: RuleTagBadgeProps) => { )), [tags, tagItemDataTestSubj] ); + if (tagsOutPopover) { + return ( + // Put 0 to fix negative left margin value. + + {tagBadges} + + ); + } return ( - + {tagTitle}
{tagBadges}
diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_rule_tag_badge.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_tag_badge.tsx index dc889402ab3c5..aa00bb9a2ac6d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/get_rule_tag_badge.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_tag_badge.tsx @@ -6,9 +6,13 @@ */ import React from 'react'; +import { + RuleTagBadgeProps, + RuleTagBadgeOptions, +} from '../application/sections/rules_list/components/rule_tag_badge'; import { RuleTagBadge } from '../application/sections'; -import type { RuleTagBadgeProps } from '../application/sections/rules_list/components/rule_tag_badge'; - -export const getRuleTagBadgeLazy = (props: RuleTagBadgeProps) => { +export const getRuleTagBadgeLazy = ( + props: RuleTagBadgeProps +) => { return ; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/mocks.ts index e05c50b71e0ed..37efdf36eeaed 100644 --- a/x-pack/plugins/triggers_actions_ui/public/mocks.ts +++ b/x-pack/plugins/triggers_actions_ui/public/mocks.ts @@ -21,6 +21,8 @@ import { RuleTypeModel, AlertsTableProps, AlertsTableConfigurationRegistry, + RuleTagBadgeOptions, + RuleTagBadgeProps, } from './types'; import { getAlertsTableLazy } from './common/get_alerts_table'; import { getRuleStatusDropdownLazy } from './common/get_rule_status_dropdown'; @@ -90,8 +92,8 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart { getRuleStatusFilter: (props) => { return getRuleStatusFilterLazy(props); }, - getRuleTagBadge: (props) => { - return getRuleTagBadgeLazy(props); + getRuleTagBadge: (props: RuleTagBadgeProps) => { + return getRuleTagBadgeLazy(props); }, getRuleEventLogList: (props) => { return getRuleEventLogListLazy(props); diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index a572a0c15030b..f830488a1ecdb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -54,6 +54,7 @@ import type { RuleTagFilterProps, RuleStatusFilterProps, RuleTagBadgeProps, + RuleTagBadgeOptions, RuleEventLogListProps, RulesListNotifyBadgeProps, AlertsTableConfigurationRegistry, @@ -98,7 +99,9 @@ export interface TriggersAndActionsUIPublicPluginStart { getRuleStatusDropdown: (props: RuleStatusDropdownProps) => ReactElement; getRuleTagFilter: (props: RuleTagFilterProps) => ReactElement; getRuleStatusFilter: (props: RuleStatusFilterProps) => ReactElement; - getRuleTagBadge: (props: RuleTagBadgeProps) => ReactElement; + getRuleTagBadge: ( + props: RuleTagBadgeProps + ) => ReactElement>; getRuleEventLogList: (props: RuleEventLogListProps) => ReactElement; getRulesListNotifyBadge: ( props: RulesListNotifyBadgeProps @@ -305,7 +308,7 @@ export class Plugin getRuleStatusFilter: (props: RuleStatusFilterProps) => { return getRuleStatusFilterLazy(props); }, - getRuleTagBadge: (props: RuleTagBadgeProps) => { + getRuleTagBadge: (props: RuleTagBadgeProps) => { return getRuleTagBadgeLazy(props); }, getRuleEventLogList: (props: RuleEventLogListProps) => { diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index d88567adf5d40..7d1e1f7bbadf6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -50,7 +50,10 @@ import { TypeRegistry } from './application/type_registry'; import type { ComponentOpts as RuleStatusDropdownProps } from './application/sections/rules_list/components/rule_status_dropdown'; import type { RuleTagFilterProps } from './application/sections/rules_list/components/rule_tag_filter'; import type { RuleStatusFilterProps } from './application/sections/rules_list/components/rule_status_filter'; -import type { RuleTagBadgeProps } from './application/sections/rules_list/components/rule_tag_badge'; +import type { + RuleTagBadgeProps, + RuleTagBadgeOptions, +} from './application/sections/rules_list/components/rule_tag_badge'; import type { RuleEventLogListProps } from './application/sections/rule_details/components/rule_event_log_list'; import type { CreateConnectorFlyoutProps } from './application/sections/action_connector_form/create_connector_flyout'; import type { EditConnectorFlyoutProps } from './application/sections/action_connector_form/edit_connector_flyout'; @@ -90,6 +93,7 @@ export type { RuleTagFilterProps, RuleStatusFilterProps, RuleTagBadgeProps, + RuleTagBadgeOptions, RuleEventLogListProps, CreateConnectorFlyoutProps, EditConnectorFlyoutProps, From 705e516701f4e0541d85e35e6cc149fd999bb618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Alvarez=20Pi=C3=B1eiro?= <95703246+emilioalvap@users.noreply.github.com> Date: Thu, 23 Jun 2022 20:26:52 +0200 Subject: [PATCH 47/54] Fix Client Metrics e2e user (#135056) --- x-pack/plugins/ux/e2e/journeys/ux_client_metrics.journey.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ux/e2e/journeys/ux_client_metrics.journey.ts b/x-pack/plugins/ux/e2e/journeys/ux_client_metrics.journey.ts index 7d0d426d5fd0d..a8abad60e3e5c 100644 --- a/x-pack/plugins/ux/e2e/journeys/ux_client_metrics.journey.ts +++ b/x-pack/plugins/ux/e2e/journeys/ux_client_metrics.journey.ts @@ -43,7 +43,7 @@ journey('UX ClientMetrics', async ({ page, params }) => { }); await loginToKibana({ page, - user: { username: 'viewer_user', password: 'changeme' }, + user: { username: 'viewer', password: 'changeme' }, }); }); From 61b5349d958a569443aea002ebaf3fcd6b3d747c Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 23 Jun 2022 14:10:57 -0500 Subject: [PATCH 48/54] [SUPT] fix apm reporting (#135071) --- .../functional/performance_playwright.sh | 31 ++++++++++++----- .../test/performance/services/performance.ts | 33 ++++++++++++++++++- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/.buildkite/scripts/steps/functional/performance_playwright.sh b/.buildkite/scripts/steps/functional/performance_playwright.sh index 9436552326d93..fbbd028f96adf 100644 --- a/.buildkite/scripts/steps/functional/performance_playwright.sh +++ b/.buildkite/scripts/steps/functional/performance_playwright.sh @@ -15,6 +15,21 @@ node scripts/es snapshot& esPid=$! +# unset env vars defined in other parts of CI for automatic APM collection of +# Kibana. We manage APM config in our FTR config and performance service, and +# APM treats config in the ENV with a very high precedence. +unset ELASTIC_APM_ENVIRONMENT +unset ELASTIC_APM_TRANSACTION_SAMPLE_RATE +unset ELASTIC_APM_SERVER_URL +unset ELASTIC_APM_SECRET_TOKEN +unset ELASTIC_APM_ACTIVE +unset ELASTIC_APM_CONTEXT_PROPAGATION_ONLY +unset ELASTIC_APM_ACTIVE +unset ELASTIC_APM_SERVER_URL +unset ELASTIC_APM_SECRET_TOKEN +unset ELASTIC_APM_GLOBAL_LABELS + + export TEST_ES_URL=http://elastic:changeme@localhost:9200 export TEST_ES_DISABLE_STARTUP=true @@ -30,19 +45,19 @@ for i in "${journeys[@]}"; do checks-reporter-with-killswitch "Run Performance Tests with Playwright Config (Journey:${i},Phase: WARMUP)" \ node scripts/functional_tests \ - --config "x-pack/test/performance/journeys/${i}/config.ts" \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --debug \ - --bail + --config "x-pack/test/performance/journeys/${i}/config.ts" \ + --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ + --debug \ + --bail export TEST_PERFORMANCE_PHASE=TEST checks-reporter-with-killswitch "Run Performance Tests with Playwright Config (Journey:${i},Phase: TEST)" \ node scripts/functional_tests \ - --config "x-pack/test/performance/journeys/${i}/config.ts" \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --debug \ - --bail + --config "x-pack/test/performance/journeys/${i}/config.ts" \ + --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ + --debug \ + --bail done kill "$esPid" diff --git a/x-pack/test/performance/services/performance.ts b/x-pack/test/performance/services/performance.ts index 3034883c93e31..2afd9f3e78393 100644 --- a/x-pack/test/performance/services/performance.ts +++ b/x-pack/test/performance/services/performance.ts @@ -9,6 +9,7 @@ import Url from 'url'; import { inspect } from 'util'; +import { setTimeout } from 'timers/promises'; import apm, { Span, Transaction } from 'elastic-apm-node'; import playwright, { ChromiumBrowser, Page, BrowserContext, CDPSession } from 'playwright'; import { FtrService, FtrProviderContext } from '../ftr_provider_context'; @@ -35,14 +36,45 @@ export class PerformanceTestingService extends FtrService { ctx.getService('lifecycle').beforeTests.add(() => { apm.start({ serviceName: 'functional test runner', + environment: process.env.CI ? 'ci' : 'development', + active: this.config.get(`kbnTestServer.env`).ELASTIC_APM_ACTIVE !== 'false', serverUrl: this.config.get(`kbnTestServer.env`).ELASTIC_APM_SERVER_URL, secretToken: this.config.get(`kbnTestServer.env`).ELASTIC_APM_SECRET_TOKEN, globalLabels: this.config.get(`kbnTestServer.env`).ELASTIC_APM_GLOBAL_LABELS, + transactionSampleRate: + this.config.get(`kbnTestServer.env`).ELASTIC_APM_TRANSACTION_SAMPLE_RATE, + logger: process.env.VERBOSE_APM_LOGGING + ? { + warn(...args: any[]) { + console.log('APM WARN', ...args); + }, + info(...args: any[]) { + console.log('APM INFO', ...args); + }, + fatal(...args: any[]) { + console.log('APM FATAL', ...args); + }, + error(...args: any[]) { + console.log('APM ERROR', ...args); + }, + debug(...args: any[]) { + console.log('APM DEBUG', ...args); + }, + trace(...args: any[]) { + console.log('APM TRACE', ...args); + }, + } + : undefined, }); }); ctx.getService('lifecycle').cleanup.add(async () => { await this.shutdownBrowser(); + await new Promise((resolve) => apm.flush(() => resolve())); + // wait for the HTTP request that apm.flush() starts, which we + // can't track but hope is complete within 3 seconds + // https://github.com/elastic/apm-agent-nodejs/issues/2088 + await setTimeout(3000); }); } @@ -173,7 +205,6 @@ export class PerformanceTestingService extends FtrService { private async tearDown(page: Page, client: CDPSession, context: BrowserContext) { if (page) { - apm.flush(); await client.detach(); await page.close(); await context.close(); From acfd0517a27b40ab047cfd44d998bff05f5dfd33 Mon Sep 17 00:00:00 2001 From: "Joey F. Poon" Date: Thu, 23 Jun 2022 14:28:03 -0500 Subject: [PATCH 49/54] [Security Solution] response action entity_id number -> string (#135037) --- .../security_solution/common/endpoint/schema/actions.test.ts | 4 ++-- .../security_solution/common/endpoint/schema/actions.ts | 2 +- .../security_solution/common/endpoint/types/actions.ts | 2 +- .../security_solution_endpoint_api_int/apis/endpoint_authz.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/actions.test.ts b/x-pack/plugins/security_solution/common/endpoint/schema/actions.test.ts index d3f47421d7d7c..a9ae8a91fe65b 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/actions.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/actions.test.ts @@ -213,7 +213,7 @@ describe('actions schemas', () => { KillOrSuspendProcessRequestSchema.body.validate({ endpoint_ids: ['ABC-XYZ-000'], parameters: { - entity_id: 5678, + entity_id: 'abc123', }, }); }).not.toThrow(); @@ -225,7 +225,7 @@ describe('actions schemas', () => { endpoint_ids: ['ABC-XYZ-000'], parameters: { pid: 1234, - entity_id: 5678, + entity_id: 'abc123', }, }); }).toThrow(); diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts index cf190e5c62ca2..600e69b83f6ef 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/actions.ts @@ -27,7 +27,7 @@ export const KillOrSuspendProcessRequestSchema = { ...BaseActionRequestSchema, parameters: schema.oneOf([ schema.object({ pid: schema.number({ min: 1 }) }), - schema.object({ entity_id: schema.number({ min: 1 }) }), + schema.object({ entity_id: schema.string({ minLength: 1 }) }), ]), }), }; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index ee07e05bac2d3..45865ad59a6c6 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -87,7 +87,7 @@ interface ResponseActionParametersWithPid { interface ResponseActionParametersWithEntityId { pid?: never; - entity_id: number; + entity_id: string; } export type ResponseActionParametersWithPidOrEntityId = diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts index c0b15090cda1e..1b5efd3654e3c 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts @@ -94,12 +94,12 @@ export default function ({ getService }: FtrProviderContext) { { method: 'post', path: KILL_PROCESS_ROUTE, - body: { endpoint_ids: ['one'], parameters: { entity_id: 1234 } }, + body: { endpoint_ids: ['one'], parameters: { entity_id: 'abc123' } }, }, { method: 'post', path: SUSPEND_PROCESS_ROUTE, - body: { endpoint_ids: ['one'], parameters: { entity_id: 1234 } }, + body: { endpoint_ids: ['one'], parameters: { entity_id: 'abc123' } }, }, ]; From f68999d631f468f34fec8ab68d0818238bfaf7c5 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Thu, 23 Jun 2022 12:29:44 -0700 Subject: [PATCH 50/54] Create packages for browser-side fatalErrors service (#134962) --- .i18nrc.json | 6 +- package.json | 6 + packages/BUILD.bazel | 6 + .../BUILD.bazel | 125 ++++++++++++++++++ .../README.md | 3 + .../jest.config.js | 13 ++ .../package.json | 8 ++ .../fatal_errors_screen.test.tsx.snap | 0 .../fatal_errors_service.test.ts.snap | 0 .../src}/fatal_errors_screen.test.tsx | 6 +- .../src}/fatal_errors_screen.tsx | 13 +- .../src}/fatal_errors_service.test.mocks.ts | 0 .../src}/fatal_errors_service.test.ts | 4 +- .../src}/fatal_errors_service.tsx | 47 ++----- .../src}/get_error_info.test.ts | 0 .../src}/get_error_info.ts | 12 +- .../src}/index.ts | 3 +- .../tsconfig.json | 18 +++ .../BUILD.bazel | 109 +++++++++++++++ .../core-fatal-errors-browser-mocks/README.md | 3 + .../jest.config.js | 13 ++ .../package.json | 8 ++ .../src}/fatal_errors_service.mock.ts | 3 +- .../src/index.ts | 9 ++ .../tsconfig.json | 18 +++ .../core-fatal-errors-browser/BUILD.bazel | 106 +++++++++++++++ .../core-fatal-errors-browser/README.md | 3 + .../core-fatal-errors-browser/jest.config.js | 13 ++ .../core-fatal-errors-browser/package.json | 8 ++ .../core-fatal-errors-browser/src/contract.ts | 38 ++++++ .../src/get_error_info.ts | 17 +++ .../core-fatal-errors-browser/src/index.ts | 10 ++ .../core-fatal-errors-browser/tsconfig.json | 18 +++ .../core-theme-browser-internal/BUILD.bazel | 1 + .../src}/core_context_provider.tsx | 2 +- .../core-theme-browser-internal/src/index.ts | 1 + src/core/public/core_system.test.mocks.ts | 4 +- src/core/public/core_system.ts | 3 +- src/core/public/http/http_service.test.ts | 2 +- src/core/public/http/http_service.ts | 2 +- .../public/http/loading_count_service.test.ts | 2 +- src/core/public/http/loading_count_service.ts | 2 +- src/core/public/index.ts | 6 +- src/core/public/kbn_bootstrap.test.mocks.ts | 2 +- src/core/public/mocks.ts | 4 +- .../notifications/toasts/toasts_service.tsx | 2 +- .../public/overlays/flyout/flyout_service.tsx | 3 +- .../public/overlays/modal/modal_service.tsx | 3 +- .../public/plugins/plugins_service.test.ts | 2 +- .../public/rendering/rendering_service.tsx | 2 +- src/core/public/utils/index.ts | 1 - src/core/test_helpers/http_test_setup.ts | 2 +- yarn.lock | 24 ++++ 53 files changed, 632 insertions(+), 84 deletions(-) create mode 100644 packages/core/fatal-errors/core-fatal-errors-browser-internal/BUILD.bazel create mode 100644 packages/core/fatal-errors/core-fatal-errors-browser-internal/README.md create mode 100644 packages/core/fatal-errors/core-fatal-errors-browser-internal/jest.config.js create mode 100644 packages/core/fatal-errors/core-fatal-errors-browser-internal/package.json rename {src/core/public/fatal_errors => packages/core/fatal-errors/core-fatal-errors-browser-internal/src}/__snapshots__/fatal_errors_screen.test.tsx.snap (100%) rename {src/core/public/fatal_errors => packages/core/fatal-errors/core-fatal-errors-browser-internal/src}/__snapshots__/fatal_errors_service.test.ts.snap (100%) rename {src/core/public/fatal_errors => packages/core/fatal-errors/core-fatal-errors-browser-internal/src}/fatal_errors_screen.test.tsx (96%) rename {src/core/public/fatal_errors => packages/core/fatal-errors/core-fatal-errors-browser-internal/src}/fatal_errors_screen.tsx (93%) rename {src/core/public/fatal_errors => packages/core/fatal-errors/core-fatal-errors-browser-internal/src}/fatal_errors_service.test.mocks.ts (100%) rename {src/core/public/fatal_errors => packages/core/fatal-errors/core-fatal-errors-browser-internal/src}/fatal_errors_service.test.ts (97%) rename {src/core/public/fatal_errors => packages/core/fatal-errors/core-fatal-errors-browser-internal/src}/fatal_errors_service.tsx (71%) rename {src/core/public/fatal_errors => packages/core/fatal-errors/core-fatal-errors-browser-internal/src}/get_error_info.test.ts (100%) rename {src/core/public/fatal_errors => packages/core/fatal-errors/core-fatal-errors-browser-internal/src}/get_error_info.ts (92%) rename {src/core/public/fatal_errors => packages/core/fatal-errors/core-fatal-errors-browser-internal/src}/index.ts (75%) create mode 100644 packages/core/fatal-errors/core-fatal-errors-browser-internal/tsconfig.json create mode 100644 packages/core/fatal-errors/core-fatal-errors-browser-mocks/BUILD.bazel create mode 100644 packages/core/fatal-errors/core-fatal-errors-browser-mocks/README.md create mode 100644 packages/core/fatal-errors/core-fatal-errors-browser-mocks/jest.config.js create mode 100644 packages/core/fatal-errors/core-fatal-errors-browser-mocks/package.json rename {src/core/public/fatal_errors => packages/core/fatal-errors/core-fatal-errors-browser-mocks/src}/fatal_errors_service.mock.ts (88%) create mode 100644 packages/core/fatal-errors/core-fatal-errors-browser-mocks/src/index.ts create mode 100644 packages/core/fatal-errors/core-fatal-errors-browser-mocks/tsconfig.json create mode 100644 packages/core/fatal-errors/core-fatal-errors-browser/BUILD.bazel create mode 100644 packages/core/fatal-errors/core-fatal-errors-browser/README.md create mode 100644 packages/core/fatal-errors/core-fatal-errors-browser/jest.config.js create mode 100644 packages/core/fatal-errors/core-fatal-errors-browser/package.json create mode 100644 packages/core/fatal-errors/core-fatal-errors-browser/src/contract.ts create mode 100644 packages/core/fatal-errors/core-fatal-errors-browser/src/get_error_info.ts create mode 100644 packages/core/fatal-errors/core-fatal-errors-browser/src/index.ts create mode 100644 packages/core/fatal-errors/core-fatal-errors-browser/tsconfig.json rename {src/core/public/utils => packages/core/theme/core-theme-browser-internal/src}/core_context_provider.tsx (95%) diff --git a/.i18nrc.json b/.i18nrc.json index 8c98fc415c910..88c1ecb5106bc 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -7,7 +7,11 @@ "bfetch": "src/plugins/bfetch", "charts": "src/plugins/charts", "console": "src/plugins/console", - "core": ["src/core", "packages/core/i18n/core-i18n-browser-internal"], + "core": [ + "src/core", + "packages/core/i18n/core-i18n-browser-internal", + "packages/core/fatal-errors/core-fatal-errors-browser-internal" + ], "customIntegrations": "src/plugins/custom_integrations", "dashboard": "src/plugins/dashboard", "controls": "src/plugins/controls", diff --git a/package.json b/package.json index 717392523c956..d9e567e45bb80 100644 --- a/package.json +++ b/package.json @@ -167,6 +167,9 @@ "@kbn/core-doc-links-server": "link:bazel-bin/packages/core/doc-links/core-doc-links-server", "@kbn/core-doc-links-server-internal": "link:bazel-bin/packages/core/doc-links/core-doc-links-server-internal", "@kbn/core-doc-links-server-mocks": "link:bazel-bin/packages/core/doc-links/core-doc-links-server-mocks", + "@kbn/core-fatal-errors-browser": "link:bazel-bin/packages/core/fatal-errors/core-fatal-errors-browser", + "@kbn/core-fatal-errors-browser-internal": "link:bazel-bin/packages/core/fatal-errors/core-fatal-errors-browser-internal", + "@kbn/core-fatal-errors-browser-mocks": "link:bazel-bin/packages/core/fatal-errors/core-fatal-errors-browser-mocks", "@kbn/core-i18n-browser": "link:bazel-bin/packages/core/i18n/core-i18n-browser", "@kbn/core-i18n-browser-internal": "link:bazel-bin/packages/core/i18n/core-i18n-browser-internal", "@kbn/core-i18n-browser-mocks": "link:bazel-bin/packages/core/i18n/core-i18n-browser-mocks", @@ -705,6 +708,9 @@ "@types/kbn__core-doc-links-server": "link:bazel-bin/packages/core/doc-links/core-doc-links-server/npm_module_types", "@types/kbn__core-doc-links-server-internal": "link:bazel-bin/packages/core/doc-links/core-doc-links-server-internal/npm_module_types", "@types/kbn__core-doc-links-server-mocks": "link:bazel-bin/packages/core/doc-links/core-doc-links-server-mocks/npm_module_types", + "@types/kbn__core-fatal-errors-browser": "link:bazel-bin/packages/core/fatal-errors/core-fatal-errors-browser/npm_module_types", + "@types/kbn__core-fatal-errors-browser-internal": "link:bazel-bin/packages/core/fatal-errors/core-fatal-errors-browser-internal/npm_module_types", + "@types/kbn__core-fatal-errors-browser-mocks": "link:bazel-bin/packages/core/fatal-errors/core-fatal-errors-browser-mocks/npm_module_types", "@types/kbn__core-i18n-browser": "link:bazel-bin/packages/core/i18n/core-i18n-browser/npm_module_types", "@types/kbn__core-i18n-browser-internal": "link:bazel-bin/packages/core/i18n/core-i18n-browser-internal/npm_module_types", "@types/kbn__core-i18n-browser-mocks": "link:bazel-bin/packages/core/i18n/core-i18n-browser-mocks/npm_module_types", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 253c5cc2b4fb3..a44ba9cd3a2d0 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -33,6 +33,9 @@ filegroup( "//packages/core/doc-links/core-doc-links-server-internal:build", "//packages/core/doc-links/core-doc-links-server-mocks:build", "//packages/core/doc-links/core-doc-links-server:build", + "//packages/core/fatal-errors/core-fatal-errors-browser-internal:build", + "//packages/core/fatal-errors/core-fatal-errors-browser-mocks:build", + "//packages/core/fatal-errors/core-fatal-errors-browser:build", "//packages/core/i18n/core-i18n-browser-internal:build", "//packages/core/i18n/core-i18n-browser-mocks:build", "//packages/core/i18n/core-i18n-browser:build", @@ -193,6 +196,9 @@ filegroup( "//packages/core/doc-links/core-doc-links-server-internal:build_types", "//packages/core/doc-links/core-doc-links-server-mocks:build_types", "//packages/core/doc-links/core-doc-links-server:build_types", + "//packages/core/fatal-errors/core-fatal-errors-browser-internal:build_types", + "//packages/core/fatal-errors/core-fatal-errors-browser-mocks:build_types", + "//packages/core/fatal-errors/core-fatal-errors-browser:build_types", "//packages/core/i18n/core-i18n-browser-internal:build_types", "//packages/core/i18n/core-i18n-browser-mocks:build_types", "//packages/core/i18n/core-i18n-browser:build_types", diff --git a/packages/core/fatal-errors/core-fatal-errors-browser-internal/BUILD.bazel b/packages/core/fatal-errors/core-fatal-errors-browser-internal/BUILD.bazel new file mode 100644 index 0000000000000..e79c856654b0c --- /dev/null +++ b/packages/core/fatal-errors/core-fatal-errors-browser-internal/BUILD.bazel @@ -0,0 +1,125 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "core-fatal-errors-browser-internal" +PKG_REQUIRE_NAME = "@kbn/core-fatal-errors-browser-internal" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +RUNTIME_DEPS = [ + "@npm//react", + "@npm//react-dom", + "@npm//rxjs", + "@npm//@elastic/eui", + "//packages/core/theme/core-theme-browser-internal", + "//packages/core/theme/core-theme-browser-mocks", + "//packages/core/injected-metadata/core-injected-metadata-browser-mocks", + "//packages/kbn-i18n-react", + "//packages/kbn-test-jest-helpers", + "//packages/kbn-test-subj-selector", +] + +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "@npm//@types/react", + "@npm//@types/react-dom", + "@npm//rxjs", + "@npm//@elastic/eui", + "//packages/core/injected-metadata/core-injected-metadata-browser-internal:npm_module_types", + "//packages/core/theme/core-theme-browser:npm_module_types", + "//packages/core/theme/core-theme-browser-internal:npm_module_types", + "//packages/core/i18n/core-i18n-browser:npm_module_types", + "//packages/core/fatal-errors/core-fatal-errors-browser:npm_module_types", + "//packages/kbn-i18n-react:npm_module_types", + "//packages/kbn-test-jest-helpers", + "//packages/kbn-test-subj-selector", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/core/fatal-errors/core-fatal-errors-browser-internal/README.md b/packages/core/fatal-errors/core-fatal-errors-browser-internal/README.md new file mode 100644 index 0000000000000..bc14a6611b730 --- /dev/null +++ b/packages/core/fatal-errors/core-fatal-errors-browser-internal/README.md @@ -0,0 +1,3 @@ +# @kbn/core-fatal-errors-browser-internal + +This package contains the implementation and internal types of the browser-side fatalErrors service. diff --git a/packages/core/fatal-errors/core-fatal-errors-browser-internal/jest.config.js b/packages/core/fatal-errors/core-fatal-errors-browser-internal/jest.config.js new file mode 100644 index 0000000000000..4098eeba3befd --- /dev/null +++ b/packages/core/fatal-errors/core-fatal-errors-browser-internal/jest.config.js @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/packages/core/fatal-errors/core-fatal-errors-browser-internal'], +}; diff --git a/packages/core/fatal-errors/core-fatal-errors-browser-internal/package.json b/packages/core/fatal-errors/core-fatal-errors-browser-internal/package.json new file mode 100644 index 0000000000000..fc5c13485540e --- /dev/null +++ b/packages/core/fatal-errors/core-fatal-errors-browser-internal/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/core-fatal-errors-browser-internal", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/src/core/public/fatal_errors/__snapshots__/fatal_errors_screen.test.tsx.snap b/packages/core/fatal-errors/core-fatal-errors-browser-internal/src/__snapshots__/fatal_errors_screen.test.tsx.snap similarity index 100% rename from src/core/public/fatal_errors/__snapshots__/fatal_errors_screen.test.tsx.snap rename to packages/core/fatal-errors/core-fatal-errors-browser-internal/src/__snapshots__/fatal_errors_screen.test.tsx.snap diff --git a/src/core/public/fatal_errors/__snapshots__/fatal_errors_service.test.ts.snap b/packages/core/fatal-errors/core-fatal-errors-browser-internal/src/__snapshots__/fatal_errors_service.test.ts.snap similarity index 100% rename from src/core/public/fatal_errors/__snapshots__/fatal_errors_service.test.ts.snap rename to packages/core/fatal-errors/core-fatal-errors-browser-internal/src/__snapshots__/fatal_errors_service.test.ts.snap diff --git a/src/core/public/fatal_errors/fatal_errors_screen.test.tsx b/packages/core/fatal-errors/core-fatal-errors-browser-internal/src/fatal_errors_screen.test.tsx similarity index 96% rename from src/core/public/fatal_errors/fatal_errors_screen.test.tsx rename to packages/core/fatal-errors/core-fatal-errors-browser-internal/src/fatal_errors_screen.test.tsx index b460e62b1151d..1849517c8cf3f 100644 --- a/src/core/public/fatal_errors/fatal_errors_screen.test.tsx +++ b/packages/core/fatal-errors/core-fatal-errors-browser-internal/src/fatal_errors_screen.test.tsx @@ -9,7 +9,7 @@ import { EuiCallOut } from '@elastic/eui'; import testSubjSelector from '@kbn/test-subj-selector'; import React from 'react'; -import * as Rx from 'rxjs'; +import { of, ReplaySubject } from 'rxjs'; import { mountWithIntl, shallowWithIntl } from '@kbn/test-jest-helpers'; import { FatalErrorsScreen } from './fatal_errors_screen'; @@ -27,7 +27,7 @@ describe('FatalErrorsScreen', () => { const defaultProps = { buildNumber: 123, kibanaVersion: 'bar', - errorInfo$: Rx.of(errorInfoFoo, errorInfoBar), + errorInfo$: of(errorInfoFoo, errorInfoBar), }; const noop = () => { @@ -67,7 +67,7 @@ describe('FatalErrorsScreen', () => { }); it('rerenders when errorInfo$ emits more errors', () => { - const errorInfo$ = new Rx.ReplaySubject(); + const errorInfo$ = new ReplaySubject(); const el = shallowWithIntl(); diff --git a/src/core/public/fatal_errors/fatal_errors_screen.tsx b/packages/core/fatal-errors/core-fatal-errors-browser-internal/src/fatal_errors_screen.tsx similarity index 93% rename from src/core/public/fatal_errors/fatal_errors_screen.tsx rename to packages/core/fatal-errors/core-fatal-errors-browser-internal/src/fatal_errors_screen.tsx index b46fdf134d0bd..6fbf6ab80aebb 100644 --- a/src/core/public/fatal_errors/fatal_errors_screen.tsx +++ b/packages/core/fatal-errors/core-fatal-errors-browser-internal/src/fatal_errors_screen.tsx @@ -17,17 +17,16 @@ import { EuiPageContent, } from '@elastic/eui'; import React from 'react'; -import * as Rx from 'rxjs'; -import { tap } from 'rxjs/operators'; +import { Observable, Subscription, merge, tap, fromEvent } from 'rxjs'; import { FormattedMessage } from '@kbn/i18n-react'; -import { FatalErrorInfo } from './get_error_info'; +import { FatalErrorInfo } from '@kbn/core-fatal-errors-browser'; interface Props { kibanaVersion: string; buildNumber: number; - errorInfo$: Rx.Observable; + errorInfo$: Observable; } interface State { @@ -39,12 +38,12 @@ export class FatalErrorsScreen extends React.Component { errors: [], }; - private subscription?: Rx.Subscription; + private subscription?: Subscription; public componentDidMount() { - this.subscription = Rx.merge( + this.subscription = merge( // reload the page if hash-based navigation is attempted - Rx.fromEvent(window, 'hashchange').pipe( + fromEvent(window, 'hashchange').pipe( tap(() => { window.location.reload(); }) diff --git a/src/core/public/fatal_errors/fatal_errors_service.test.mocks.ts b/packages/core/fatal-errors/core-fatal-errors-browser-internal/src/fatal_errors_service.test.mocks.ts similarity index 100% rename from src/core/public/fatal_errors/fatal_errors_service.test.mocks.ts rename to packages/core/fatal-errors/core-fatal-errors-browser-internal/src/fatal_errors_service.test.mocks.ts diff --git a/src/core/public/fatal_errors/fatal_errors_service.test.ts b/packages/core/fatal-errors/core-fatal-errors-browser-internal/src/fatal_errors_service.test.ts similarity index 97% rename from src/core/public/fatal_errors/fatal_errors_service.test.ts rename to packages/core/fatal-errors/core-fatal-errors-browser-internal/src/fatal_errors_service.test.ts index fe3e623b00f91..cc7da2e0eb353 100644 --- a/src/core/public/fatal_errors/fatal_errors_service.test.ts +++ b/packages/core/fatal-errors/core-fatal-errors-browser-internal/src/fatal_errors_service.test.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ -import * as Rx from 'rxjs'; +import { Observable } from 'rxjs'; expect.addSnapshotSerializer({ - test: (val) => val instanceof Rx.Observable, + test: (val) => val instanceof Observable, print: () => `Rx.Observable`, }); diff --git a/src/core/public/fatal_errors/fatal_errors_service.tsx b/packages/core/fatal-errors/core-fatal-errors-browser-internal/src/fatal_errors_service.tsx similarity index 71% rename from src/core/public/fatal_errors/fatal_errors_service.tsx rename to packages/core/fatal-errors/core-fatal-errors-browser-internal/src/fatal_errors_service.tsx index 952740c414163..cefa6e7c0ecb2 100644 --- a/src/core/public/fatal_errors/fatal_errors_service.tsx +++ b/packages/core/fatal-errors/core-fatal-errors-browser-internal/src/fatal_errors_service.tsx @@ -8,55 +8,26 @@ import React from 'react'; import { render } from 'react-dom'; -import * as Rx from 'rxjs'; -import { first, tap } from 'rxjs/operators'; +import { ReplaySubject, first, tap } from 'rxjs'; import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal'; import type { ThemeServiceSetup } from '@kbn/core-theme-browser'; import type { I18nStart } from '@kbn/core-i18n-browser'; -import { CoreContextProvider } from '../utils'; +import type { FatalErrorInfo, FatalErrorsSetup } from '@kbn/core-fatal-errors-browser'; +import { CoreContextProvider } from '@kbn/core-theme-browser-internal'; import { FatalErrorsScreen } from './fatal_errors_screen'; -import { FatalErrorInfo, getErrorInfo } from './get_error_info'; +import { getErrorInfo } from './get_error_info'; -export interface Deps { +/** @internal */ +export interface FatalErrorsServiceSetupDeps { i18n: I18nStart; theme: ThemeServiceSetup; injectedMetadata: InternalInjectedMetadataSetup; } -/** - * FatalErrors stop the Kibana Public Core and displays a fatal error screen - * with details about the Kibana build and the error. - * - * @public - */ -export interface FatalErrorsSetup { - /** - * Add a new fatal error. This will stop the Kibana Public Core and display - * a fatal error screen with details about the Kibana build and the error. - * - * @param error - The error to display - * @param source - Adds a prefix of the form `${source}: ` to the error message - */ - add: (error: string | Error, source?: string) => never; - - /** - * An Observable that will emit whenever a fatal error is added with `add()` - */ - get$: () => Rx.Observable; -} - -/** - * FatalErrors stop the Kibana Public Core and displays a fatal error screen - * with details about the Kibana build and the error. - * - * @public - */ -export type FatalErrorsStart = FatalErrorsSetup; - /** @internal */ export class FatalErrorsService { - private readonly errorInfo$ = new Rx.ReplaySubject(); + private readonly errorInfo$ = new ReplaySubject(); private fatalErrors?: FatalErrorsSetup; /** @@ -67,7 +38,7 @@ export class FatalErrorsService { */ constructor(private rootDomElement: HTMLElement, private onFirstErrorCb: () => void) {} - public setup(deps: Deps) { + public setup(deps: FatalErrorsServiceSetupDeps) { this.errorInfo$ .pipe( first(), @@ -115,7 +86,7 @@ export class FatalErrorsService { return fatalErrors; } - private renderError({ i18n, theme, injectedMetadata }: Deps) { + private renderError({ i18n, theme, injectedMetadata }: FatalErrorsServiceSetupDeps) { // delete all content in the rootDomElement this.rootDomElement.textContent = ''; diff --git a/src/core/public/fatal_errors/get_error_info.test.ts b/packages/core/fatal-errors/core-fatal-errors-browser-internal/src/get_error_info.test.ts similarity index 100% rename from src/core/public/fatal_errors/get_error_info.test.ts rename to packages/core/fatal-errors/core-fatal-errors-browser-internal/src/get_error_info.test.ts diff --git a/src/core/public/fatal_errors/get_error_info.ts b/packages/core/fatal-errors/core-fatal-errors-browser-internal/src/get_error_info.ts similarity index 92% rename from src/core/public/fatal_errors/get_error_info.ts rename to packages/core/fatal-errors/core-fatal-errors-browser-internal/src/get_error_info.ts index 89ecb3f4a918f..186011b778eda 100644 --- a/src/core/public/fatal_errors/get_error_info.ts +++ b/packages/core/fatal-errors/core-fatal-errors-browser-internal/src/get_error_info.ts @@ -7,7 +7,7 @@ */ import { inspect } from 'util'; - +import type { FatalErrorInfo } from '@kbn/core-fatal-errors-browser'; /** * Produce a string version of an error, */ @@ -63,13 +63,3 @@ export function getErrorInfo(error: any, source?: string): FatalErrorInfo { stack: formatStack(error), }; } - -/** - * Represents the `message` and `stack` of a fatal Error - * - * @public - * */ -export interface FatalErrorInfo { - message: string; - stack: string | undefined; -} diff --git a/src/core/public/fatal_errors/index.ts b/packages/core/fatal-errors/core-fatal-errors-browser-internal/src/index.ts similarity index 75% rename from src/core/public/fatal_errors/index.ts rename to packages/core/fatal-errors/core-fatal-errors-browser-internal/src/index.ts index bb6590c691296..20bd9db4936d4 100644 --- a/src/core/public/fatal_errors/index.ts +++ b/packages/core/fatal-errors/core-fatal-errors-browser-internal/src/index.ts @@ -7,5 +7,4 @@ */ export { FatalErrorsService } from './fatal_errors_service'; -export type { FatalErrorsSetup, FatalErrorsStart } from './fatal_errors_service'; -export type { FatalErrorInfo } from './get_error_info'; +export type { FatalErrorsServiceSetupDeps } from './fatal_errors_service'; diff --git a/packages/core/fatal-errors/core-fatal-errors-browser-internal/tsconfig.json b/packages/core/fatal-errors/core-fatal-errors-browser-internal/tsconfig.json new file mode 100644 index 0000000000000..dc20b641b1989 --- /dev/null +++ b/packages/core/fatal-errors/core-fatal-errors-browser-internal/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/core/fatal-errors/core-fatal-errors-browser-mocks/BUILD.bazel b/packages/core/fatal-errors/core-fatal-errors-browser-mocks/BUILD.bazel new file mode 100644 index 0000000000000..647682e9f5b98 --- /dev/null +++ b/packages/core/fatal-errors/core-fatal-errors-browser-mocks/BUILD.bazel @@ -0,0 +1,109 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "core-fatal-errors-browser-mocks" +PKG_REQUIRE_NAME = "@kbn/core-fatal-errors-browser-mocks" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +RUNTIME_DEPS = [ + "@npm//react", + "//packages/core/fatal-errors/core-fatal-errors-browser-internal", +] + +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "@npm//@types/react", + "//packages/core/fatal-errors/core-fatal-errors-browser:npm_module_types", + "//packages/core/fatal-errors/core-fatal-errors-browser-internal:npm_module_types", + "//packages/kbn-utility-types:npm_module_types" +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/core/fatal-errors/core-fatal-errors-browser-mocks/README.md b/packages/core/fatal-errors/core-fatal-errors-browser-mocks/README.md new file mode 100644 index 0000000000000..9f532282c5535 --- /dev/null +++ b/packages/core/fatal-errors/core-fatal-errors-browser-mocks/README.md @@ -0,0 +1,3 @@ +# @kbn/core-fatal-errors-browser-mocks + +This package contains the mocks for Core's fatalErrors service. diff --git a/packages/core/fatal-errors/core-fatal-errors-browser-mocks/jest.config.js b/packages/core/fatal-errors/core-fatal-errors-browser-mocks/jest.config.js new file mode 100644 index 0000000000000..2aebfcfd4f993 --- /dev/null +++ b/packages/core/fatal-errors/core-fatal-errors-browser-mocks/jest.config.js @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/packages/core/fatal-errors/core-fatal-errors-browser-mocks'], +}; diff --git a/packages/core/fatal-errors/core-fatal-errors-browser-mocks/package.json b/packages/core/fatal-errors/core-fatal-errors-browser-mocks/package.json new file mode 100644 index 0000000000000..208ee92ee8367 --- /dev/null +++ b/packages/core/fatal-errors/core-fatal-errors-browser-mocks/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/core-fatal-errors-browser-mocks", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/src/core/public/fatal_errors/fatal_errors_service.mock.ts b/packages/core/fatal-errors/core-fatal-errors-browser-mocks/src/fatal_errors_service.mock.ts similarity index 88% rename from src/core/public/fatal_errors/fatal_errors_service.mock.ts rename to packages/core/fatal-errors/core-fatal-errors-browser-mocks/src/fatal_errors_service.mock.ts index 151164000bfb3..222fb29ebc811 100644 --- a/src/core/public/fatal_errors/fatal_errors_service.mock.ts +++ b/packages/core/fatal-errors/core-fatal-errors-browser-mocks/src/fatal_errors_service.mock.ts @@ -7,7 +7,8 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { FatalErrorsService, FatalErrorsSetup } from './fatal_errors_service'; +import type { FatalErrorsSetup } from '@kbn/core-fatal-errors-browser'; +import { FatalErrorsService } from '@kbn/core-fatal-errors-browser-internal'; const createSetupContractMock = () => { const setupContract: jest.Mocked = { diff --git a/packages/core/fatal-errors/core-fatal-errors-browser-mocks/src/index.ts b/packages/core/fatal-errors/core-fatal-errors-browser-mocks/src/index.ts new file mode 100644 index 0000000000000..a31ac652c885d --- /dev/null +++ b/packages/core/fatal-errors/core-fatal-errors-browser-mocks/src/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { fatalErrorsServiceMock } from './fatal_errors_service.mock'; diff --git a/packages/core/fatal-errors/core-fatal-errors-browser-mocks/tsconfig.json b/packages/core/fatal-errors/core-fatal-errors-browser-mocks/tsconfig.json new file mode 100644 index 0000000000000..dc20b641b1989 --- /dev/null +++ b/packages/core/fatal-errors/core-fatal-errors-browser-mocks/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/core/fatal-errors/core-fatal-errors-browser/BUILD.bazel b/packages/core/fatal-errors/core-fatal-errors-browser/BUILD.bazel new file mode 100644 index 0000000000000..5cd525836c328 --- /dev/null +++ b/packages/core/fatal-errors/core-fatal-errors-browser/BUILD.bazel @@ -0,0 +1,106 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "core-fatal-errors-browser" +PKG_REQUIRE_NAME = "@kbn/core-fatal-errors-browser" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +RUNTIME_DEPS = [ + "@npm//react" +] + +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "@npm//@types/react", + "@npm//rxjs", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/core/fatal-errors/core-fatal-errors-browser/README.md b/packages/core/fatal-errors/core-fatal-errors-browser/README.md new file mode 100644 index 0000000000000..a0d4ead4c4837 --- /dev/null +++ b/packages/core/fatal-errors/core-fatal-errors-browser/README.md @@ -0,0 +1,3 @@ +# @kbn/core-fatal-errors-browser + +This package contains the browser public types for the fatalErrors core service. diff --git a/packages/core/fatal-errors/core-fatal-errors-browser/jest.config.js b/packages/core/fatal-errors/core-fatal-errors-browser/jest.config.js new file mode 100644 index 0000000000000..aef7abddb0e6e --- /dev/null +++ b/packages/core/fatal-errors/core-fatal-errors-browser/jest.config.js @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/packages/core/fatal-errors/core-fatal-errors-browser'], +}; diff --git a/packages/core/fatal-errors/core-fatal-errors-browser/package.json b/packages/core/fatal-errors/core-fatal-errors-browser/package.json new file mode 100644 index 0000000000000..d45515d4d9f92 --- /dev/null +++ b/packages/core/fatal-errors/core-fatal-errors-browser/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/core-fatal-errors-browser", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/core/fatal-errors/core-fatal-errors-browser/src/contract.ts b/packages/core/fatal-errors/core-fatal-errors-browser/src/contract.ts new file mode 100644 index 0000000000000..2b6bc8ee0abad --- /dev/null +++ b/packages/core/fatal-errors/core-fatal-errors-browser/src/contract.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import type { Observable } from 'rxjs'; +import type { FatalErrorInfo } from './get_error_info'; +/** + * FatalErrors stop the Kibana Public Core and displays a fatal error screen + * with details about the Kibana build and the error. + * + * @public + */ +export interface FatalErrorsSetup { + /** + * Add a new fatal error. This will stop the Kibana Public Core and display + * a fatal error screen with details about the Kibana build and the error. + * + * @param error - The error to display + * @param source - Adds a prefix of the form `${source}: ` to the error message + */ + add: (error: string | Error, source?: string) => never; + + /** + * An Observable that will emit whenever a fatal error is added with `add()` + */ + get$: () => Observable; +} + +/** + * FatalErrors stop the Kibana Public Core and displays a fatal error screen + * with details about the Kibana build and the error. + * + * @public + */ +export type FatalErrorsStart = FatalErrorsSetup; diff --git a/packages/core/fatal-errors/core-fatal-errors-browser/src/get_error_info.ts b/packages/core/fatal-errors/core-fatal-errors-browser/src/get_error_info.ts new file mode 100644 index 0000000000000..a0e0b67b24d29 --- /dev/null +++ b/packages/core/fatal-errors/core-fatal-errors-browser/src/get_error_info.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * Represents the `message` and `stack` of a fatal Error + * + * @public + * */ +export interface FatalErrorInfo { + message: string; + stack: string | undefined; +} diff --git a/packages/core/fatal-errors/core-fatal-errors-browser/src/index.ts b/packages/core/fatal-errors/core-fatal-errors-browser/src/index.ts new file mode 100644 index 0000000000000..365e8ab268df8 --- /dev/null +++ b/packages/core/fatal-errors/core-fatal-errors-browser/src/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type { FatalErrorsSetup, FatalErrorsStart } from './contract'; +export type { FatalErrorInfo } from './get_error_info'; diff --git a/packages/core/fatal-errors/core-fatal-errors-browser/tsconfig.json b/packages/core/fatal-errors/core-fatal-errors-browser/tsconfig.json new file mode 100644 index 0000000000000..dc20b641b1989 --- /dev/null +++ b/packages/core/fatal-errors/core-fatal-errors-browser/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/core/theme/core-theme-browser-internal/BUILD.bazel b/packages/core/theme/core-theme-browser-internal/BUILD.bazel index e4e56b6ef38f3..4dc740ef5804d 100644 --- a/packages/core/theme/core-theme-browser-internal/BUILD.bazel +++ b/packages/core/theme/core-theme-browser-internal/BUILD.bazel @@ -46,6 +46,7 @@ TYPES_DEPS = [ "//packages/core/base/core-base-common:npm_module_types", "//packages/core/injected-metadata/core-injected-metadata-browser-internal:npm_module_types", "//packages/core/theme/core-theme-browser:npm_module_types", + "//packages/core/i18n/core-i18n-browser:npm_module_types", ] jsts_transpiler( diff --git a/src/core/public/utils/core_context_provider.tsx b/packages/core/theme/core-theme-browser-internal/src/core_context_provider.tsx similarity index 95% rename from src/core/public/utils/core_context_provider.tsx rename to packages/core/theme/core-theme-browser-internal/src/core_context_provider.tsx index a400f8eae9916..0c1f8aca9e2cc 100644 --- a/src/core/public/utils/core_context_provider.tsx +++ b/packages/core/theme/core-theme-browser-internal/src/core_context_provider.tsx @@ -8,8 +8,8 @@ import React, { FC } from 'react'; import type { ThemeServiceStart } from '@kbn/core-theme-browser'; -import { CoreThemeProvider } from '@kbn/core-theme-browser-internal'; import type { I18nStart } from '@kbn/core-i18n-browser'; +import { CoreThemeProvider } from './core_theme_provider'; interface CoreContextProviderProps { theme: ThemeServiceStart; diff --git a/packages/core/theme/core-theme-browser-internal/src/index.ts b/packages/core/theme/core-theme-browser-internal/src/index.ts index 8e698a6bc226e..48cbb5c6848d6 100644 --- a/packages/core/theme/core-theme-browser-internal/src/index.ts +++ b/packages/core/theme/core-theme-browser-internal/src/index.ts @@ -9,3 +9,4 @@ export { ThemeService } from './theme_service'; export { CoreThemeProvider } from './core_theme_provider'; export type { ThemeServiceSetupDeps } from './theme_service'; +export { CoreContextProvider } from './core_context_provider'; diff --git a/src/core/public/core_system.test.mocks.ts b/src/core/public/core_system.test.mocks.ts index 25bbe39ddcc25..9899e6c345ab8 100644 --- a/src/core/public/core_system.test.mocks.ts +++ b/src/core/public/core_system.test.mocks.ts @@ -12,7 +12,7 @@ import { themeServiceMock } from '@kbn/core-theme-browser-mocks'; import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks'; import { applicationServiceMock } from './application/application_service.mock'; import { chromeServiceMock } from './chrome/chrome_service.mock'; -import { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock'; +import { fatalErrorsServiceMock } from '@kbn/core-fatal-errors-browser-mocks'; import { httpServiceMock } from './http/http_service.mock'; import { i18nServiceMock } from '@kbn/core-i18n-browser-mocks'; import { notificationServiceMock } from './notifications/notifications_service.mock'; @@ -48,7 +48,7 @@ export const MockFatalErrorsService = fatalErrorsServiceMock.create(); export const FatalErrorsServiceConstructor = jest .fn() .mockImplementation(() => MockFatalErrorsService); -jest.doMock('./fatal_errors', () => ({ +jest.doMock('@kbn/core-fatal-errors-browser-internal', () => ({ FatalErrorsService: FatalErrorsServiceConstructor, })); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index bdf94d953b659..1dbb16b274ff4 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -18,9 +18,10 @@ import { ThemeService } from '@kbn/core-theme-browser-internal'; import type { AnalyticsServiceSetup, AnalyticsServiceStart } from '@kbn/core-analytics-browser'; import { AnalyticsService } from '@kbn/core-analytics-browser-internal'; import { I18nService } from '@kbn/core-i18n-browser-internal'; +import type { FatalErrorsSetup } from '@kbn/core-fatal-errors-browser'; +import { FatalErrorsService } from '@kbn/core-fatal-errors-browser-internal'; import { CoreSetup, CoreStart } from '.'; import { ChromeService } from './chrome'; -import { FatalErrorsService, FatalErrorsSetup } from './fatal_errors'; import { HttpService } from './http'; import { NotificationsService } from './notifications'; import { OverlayService } from './overlays'; diff --git a/src/core/public/http/http_service.test.ts b/src/core/public/http/http_service.test.ts index af6e2343d5f8a..0a844dfe2f4be 100644 --- a/src/core/public/http/http_service.test.ts +++ b/src/core/public/http/http_service.test.ts @@ -10,7 +10,7 @@ import fetchMock from 'fetch-mock/es5/client'; import { loadingServiceMock } from './http_service.test.mocks'; -import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; +import { fatalErrorsServiceMock } from '@kbn/core-fatal-errors-browser-mocks'; import { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks'; import { HttpService } from './http_service'; import { Observable } from 'rxjs'; diff --git a/src/core/public/http/http_service.ts b/src/core/public/http/http_service.ts index 4507b808e5a4a..57e5bc7ef2d75 100644 --- a/src/core/public/http/http_service.ts +++ b/src/core/public/http/http_service.ts @@ -8,8 +8,8 @@ import type { CoreService } from '@kbn/core-base-browser-internal'; import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal'; +import type { FatalErrorsSetup } from '@kbn/core-fatal-errors-browser'; import { HttpSetup, HttpStart } from './types'; -import { FatalErrorsSetup } from '../fatal_errors'; import { BasePath } from './base_path'; import { AnonymousPathsService } from './anonymous_paths_service'; import { LoadingCountService } from './loading_count_service'; diff --git a/src/core/public/http/loading_count_service.test.ts b/src/core/public/http/loading_count_service.test.ts index e933e93b4bde2..041af203c8b37 100644 --- a/src/core/public/http/loading_count_service.test.ts +++ b/src/core/public/http/loading_count_service.test.ts @@ -9,7 +9,7 @@ import { Observable, throwError, of, Subject } from 'rxjs'; import { toArray } from 'rxjs/operators'; -import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; +import { fatalErrorsServiceMock } from '@kbn/core-fatal-errors-browser-mocks'; import { LoadingCountService } from './loading_count_service'; describe('LoadingCountService', () => { diff --git a/src/core/public/http/loading_count_service.ts b/src/core/public/http/loading_count_service.ts index c2ae7433b82c4..4df60e53f9c66 100644 --- a/src/core/public/http/loading_count_service.ts +++ b/src/core/public/http/loading_count_service.ts @@ -17,7 +17,7 @@ import { tap, } from 'rxjs/operators'; import type { CoreService } from '@kbn/core-base-browser-internal'; -import { FatalErrorsSetup } from '../fatal_errors'; +import type { FatalErrorsSetup } from '@kbn/core-fatal-errors-browser'; /** @public */ export interface LoadingCountSetup { diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 5162cdac1024d..e16aa3ec174c1 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -35,6 +35,11 @@ import type { ThemeServiceSetup, ThemeServiceStart } from '@kbn/core-theme-brows import type { AnalyticsServiceSetup, AnalyticsServiceStart } from '@kbn/core-analytics-browser'; import type { I18nStart } from '@kbn/core-i18n-browser'; +import type { + FatalErrorsSetup, + FatalErrorsStart, + FatalErrorInfo, +} from '@kbn/core-fatal-errors-browser'; import { ChromeBadge, ChromeBreadcrumb, @@ -57,7 +62,6 @@ import { NavType, ChromeHelpMenuActions, } from './chrome'; -import { FatalErrorsSetup, FatalErrorsStart, FatalErrorInfo } from './fatal_errors'; import { HttpSetup, HttpStart } from './http'; import { NotificationsSetup, NotificationsStart } from './notifications'; import { OverlayStart } from './overlays'; diff --git a/src/core/public/kbn_bootstrap.test.mocks.ts b/src/core/public/kbn_bootstrap.test.mocks.ts index b1b48b1a21ab8..0e3a9ca95d35a 100644 --- a/src/core/public/kbn_bootstrap.test.mocks.ts +++ b/src/core/public/kbn_bootstrap.test.mocks.ts @@ -7,7 +7,7 @@ */ import { applicationServiceMock } from './application/application_service.mock'; -import { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock'; +import { fatalErrorsServiceMock } from '@kbn/core-fatal-errors-browser-mocks'; export const fatalErrorMock = fatalErrorsServiceMock.createSetupContract(); export const coreSystemMock = { setup: jest.fn().mockResolvedValue({ diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 3a1f03f5ea782..c905526fc257f 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -15,12 +15,12 @@ import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks'; // Only import types from '.' to avoid triggering default Jest mocks. import { i18nServiceMock } from '@kbn/core-i18n-browser-mocks'; +import { fatalErrorsServiceMock } from '@kbn/core-fatal-errors-browser-mocks'; import { PluginInitializerContext, AppMountParameters } from '.'; // Import values from their individual modules instead. import { ScopedHistory } from './application'; import { applicationServiceMock } from './application/application_service.mock'; import { chromeServiceMock } from './chrome/chrome_service.mock'; -import { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock'; import { httpServiceMock } from './http/http_service.mock'; import { notificationServiceMock } from './notifications/notifications_service.mock'; import { overlayServiceMock } from './overlays/overlay_service.mock'; @@ -35,7 +35,7 @@ export { themeServiceMock } from '@kbn/core-theme-browser-mocks'; export { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks'; export { chromeServiceMock } from './chrome/chrome_service.mock'; export { executionContextServiceMock } from './execution_context/execution_context_service.mock'; -export { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock'; +export { fatalErrorsServiceMock } from '@kbn/core-fatal-errors-browser-mocks'; export { httpServiceMock } from './http/http_service.mock'; export { i18nServiceMock } from '@kbn/core-i18n-browser-mocks'; export { notificationServiceMock } from './notifications/notifications_service.mock'; diff --git a/src/core/public/notifications/toasts/toasts_service.tsx b/src/core/public/notifications/toasts/toasts_service.tsx index 045da1fc24d02..63edd56085797 100644 --- a/src/core/public/notifications/toasts/toasts_service.tsx +++ b/src/core/public/notifications/toasts/toasts_service.tsx @@ -11,11 +11,11 @@ import { render, unmountComponentAtNode } from 'react-dom'; import type { ThemeServiceStart } from '@kbn/core-theme-browser'; import type { I18nStart } from '@kbn/core-i18n-browser'; +import { CoreContextProvider } from '@kbn/core-theme-browser-internal'; import { IUiSettingsClient } from '../../ui_settings'; import { GlobalToastList } from './global_toast_list'; import { ToastsApi, IToasts } from './toasts_api'; import { OverlayStart } from '../../overlays'; -import { CoreContextProvider } from '../../utils'; interface SetupDeps { uiSettings: IUiSettingsClient; diff --git a/src/core/public/overlays/flyout/flyout_service.tsx b/src/core/public/overlays/flyout/flyout_service.tsx index 701915113bb5a..186517890412d 100644 --- a/src/core/public/overlays/flyout/flyout_service.tsx +++ b/src/core/public/overlays/flyout/flyout_service.tsx @@ -14,9 +14,10 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { Subject } from 'rxjs'; import type { ThemeServiceStart } from '@kbn/core-theme-browser'; import type { I18nStart } from '@kbn/core-i18n-browser'; +import { CoreContextProvider } from '@kbn/core-theme-browser-internal'; import { MountPoint } from '../../types'; import { OverlayRef } from '../types'; -import { MountWrapper, CoreContextProvider } from '../../utils'; +import { MountWrapper } from '../../utils'; /** * A FlyoutRef is a reference to an opened flyout panel. It offers methods to diff --git a/src/core/public/overlays/modal/modal_service.tsx b/src/core/public/overlays/modal/modal_service.tsx index 0195fb2b5bf1f..5645ed5c2b8a1 100644 --- a/src/core/public/overlays/modal/modal_service.tsx +++ b/src/core/public/overlays/modal/modal_service.tsx @@ -15,9 +15,10 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { Subject } from 'rxjs'; import type { ThemeServiceStart } from '@kbn/core-theme-browser'; import type { I18nStart } from '@kbn/core-i18n-browser'; +import { CoreContextProvider } from '@kbn/core-theme-browser-internal'; import { MountPoint } from '../../types'; import { OverlayRef } from '../types'; -import { MountWrapper, CoreContextProvider } from '../../utils'; +import { MountWrapper } from '../../utils'; /** * A ModalRef is a reference to an opened modal. It offers methods to diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index b3d0ab8f295a5..98320dda7a3ae 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -30,7 +30,7 @@ import { applicationServiceMock } from '../application/application_service.mock' import { i18nServiceMock } from '@kbn/core-i18n-browser-mocks'; import { overlayServiceMock } from '../overlays/overlay_service.mock'; import { chromeServiceMock } from '../chrome/chrome_service.mock'; -import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; +import { fatalErrorsServiceMock } from '@kbn/core-fatal-errors-browser-mocks'; import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks'; import { httpServiceMock } from '../http/http_service.mock'; diff --git a/src/core/public/rendering/rendering_service.tsx b/src/core/public/rendering/rendering_service.tsx index 48031db64897a..1a656877d924a 100644 --- a/src/core/public/rendering/rendering_service.tsx +++ b/src/core/public/rendering/rendering_service.tsx @@ -12,10 +12,10 @@ import { pairwise, startWith } from 'rxjs/operators'; import type { ThemeServiceStart } from '@kbn/core-theme-browser'; import type { I18nStart } from '@kbn/core-i18n-browser'; +import { CoreContextProvider } from '@kbn/core-theme-browser-internal'; import type { InternalChromeStart } from '../chrome'; import type { InternalApplicationStart } from '../application'; import type { OverlayStart } from '../overlays'; -import { CoreContextProvider } from '../utils'; import { AppWrapper } from './app_containers'; export interface StartDeps { diff --git a/src/core/public/utils/index.ts b/src/core/public/utils/index.ts index 4fb9c50f715c6..812862c499dab 100644 --- a/src/core/public/utils/index.ts +++ b/src/core/public/utils/index.ts @@ -8,5 +8,4 @@ export { Sha256 } from './crypto'; export { MountWrapper, mountReactNode } from './mount'; -export { CoreContextProvider } from './core_context_provider'; export { KBN_LOAD_MARKS } from './consts'; diff --git a/src/core/test_helpers/http_test_setup.ts b/src/core/test_helpers/http_test_setup.ts index 2a7d6451319cb..6adc0be8b744d 100644 --- a/src/core/test_helpers/http_test_setup.ts +++ b/src/core/test_helpers/http_test_setup.ts @@ -7,8 +7,8 @@ */ import { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks'; +import { fatalErrorsServiceMock } from '@kbn/core-fatal-errors-browser-mocks'; import { HttpService } from '../public/http'; -import { fatalErrorsServiceMock } from '../public/fatal_errors/fatal_errors_service.mock'; import { executionContextServiceMock } from '../public/execution_context/execution_context_service.mock'; export type SetupTap = ( diff --git a/yarn.lock b/yarn.lock index 5233110b1f99f..fbbbbf9b85d6d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3099,6 +3099,18 @@ version "0.0.0" uid "" +"@kbn/core-fatal-errors-browser-internal@link:bazel-bin/packages/core/fatal-errors/core-fatal-errors-browser-internal": + version "0.0.0" + uid "" + +"@kbn/core-fatal-errors-browser-mocks@link:bazel-bin/packages/core/fatal-errors/core-fatal-errors-browser-mocks": + version "0.0.0" + uid "" + +"@kbn/core-fatal-errors-browser@link:bazel-bin/packages/core/fatal-errors/core-fatal-errors-browser": + version "0.0.0" + uid "" + "@kbn/core-i18n-browser-internal@link:bazel-bin/packages/core/i18n/core-i18n-browser-internal": version "0.0.0" uid "" @@ -6542,6 +6554,18 @@ version "0.0.0" uid "" +"@types/kbn__core-fatal-errors-browser-internal@link:bazel-bin/packages/core/fatal-errors/core-fatal-errors-browser-internal/npm_module_types": + version "0.0.0" + uid "" + +"@types/kbn__core-fatal-errors-browser-mocks@link:bazel-bin/packages/core/fatal-errors/core-fatal-errors-browser-mocks/npm_module_types": + version "0.0.0" + uid "" + +"@types/kbn__core-fatal-errors-browser@link:bazel-bin/packages/core/fatal-errors/core-fatal-errors-browser/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__core-i18n-browser-internal@link:bazel-bin/packages/core/i18n/core-i18n-browser-internal/npm_module_types": version "0.0.0" uid "" From aeab1635303a69c4852d2f386daff87cee53a7a5 Mon Sep 17 00:00:00 2001 From: "Joey F. Poon" Date: Thu, 23 Jun 2022 14:32:38 -0500 Subject: [PATCH 51/54] [Security Solution] response actions - enhance ActionDetail interface (#134966) * add new properties * comment * parameters * createdBy * remove logEntries property --- .../endpoint_action_generator.ts | 77 +++---------------- .../data_generators/fleet_action_generator.ts | 14 ++-- .../common/endpoint/types/actions.ts | 26 ++++--- .../action_responder/run_in_auto_mode.ts | 16 +--- .../routes/actions/response_actions.ts | 3 +- .../actions/action_details_by_id.test.ts | 72 ++--------------- .../services/actions/action_details_by_id.ts | 9 +-- .../services/actions/action_list.test.ts | 72 ++--------------- .../endpoint/services/actions/action_list.ts | 15 +--- .../endpoint/services/actions/utils.test.ts | 6 +- .../server/endpoint/services/actions/utils.ts | 7 +- .../endpoint/utils/action_list_helpers.ts | 12 --- 12 files changed, 64 insertions(+), 265 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts index 745b2277b04c2..838f24548e8aa 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts @@ -16,13 +16,11 @@ import { EndpointActivityLogAction, EndpointActivityLogActionResponse, EndpointPendingActions, - ISOLATION_ACTIONS, LogsEndpointAction, LogsEndpointActionResponse, + RESPONSE_ACTION_COMMANDS, } from '../types'; -const ISOLATION_COMMANDS: ISOLATION_ACTIONS[] = ['isolate', 'unisolate']; - export class EndpointActionGenerator extends BaseDataGenerator { /** Generate a random endpoint Action request (isolate or unisolate) */ generate(overrides: DeepPartial = {}): LogsEndpointAction { @@ -42,8 +40,9 @@ export class EndpointActionGenerator extends BaseDataGenerator { type: 'INPUT_ACTION', input_type: 'endpoint', data: { - command: this.randomIsolateCommand(), + command: this.randomResponseActionCommand(), comment: this.randomString(15), + parameters: undefined, }, }, error: undefined, @@ -87,8 +86,9 @@ export class EndpointActionGenerator extends BaseDataGenerator { action_id: this.seededUUIDv4(), completed_at: timeStamp.toISOString(), data: { - command: this.randomIsolateCommand(), + command: this.randomResponseActionCommand(), comment: '', + parameters: undefined, }, started_at: this.randomPastDate(), }, @@ -116,67 +116,10 @@ export class EndpointActionGenerator extends BaseDataGenerator { isExpired: false, wasSuccessful: true, errors: undefined, - logEntries: [ - { - item: { - data: { - '@timestamp': '2022-04-27T16:08:47.449Z', - action_id: '123', - agents: ['agent-a'], - data: { - command: 'isolate', - comment: '5wb6pu6kh2xix5i', - }, - expiration: '2022-04-29T16:08:47.449Z', - input_type: 'endpoint', - type: 'INPUT_ACTION', - user_id: 'elastic', - }, - id: '44d8b915-c69c-4c48-8c86-b57d0bd631d0', - }, - type: 'fleetAction', - }, - { - item: { - data: { - '@timestamp': '2022-04-30T16:08:47.449Z', - action_data: { - command: 'unisolate', - comment: '', - }, - action_id: '123', - agent_id: 'agent-a', - completed_at: '2022-04-30T16:08:47.449Z', - error: '', - started_at: '2022-04-30T16:08:47.449Z', - }, - id: '54-65-65-98', - }, - type: 'fleetResponse', - }, - { - item: { - data: { - '@timestamp': '2022-04-30T16:08:47.449Z', - EndpointActions: { - action_id: '123', - completed_at: '2022-04-30T16:08:47.449Z', - data: { - command: 'unisolate', - comment: '', - }, - started_at: '2022-04-30T16:08:47.449Z', - }, - agent: { - id: 'agent-a', - }, - }, - id: '32-65-98', - }, - type: 'response', - }, - ], startedAt: '2022-04-27T16:08:47.449Z', + comment: 'thisisacomment', + createdBy: 'auserid', + parameters: undefined, }; return merge(details, overrides); @@ -235,7 +178,7 @@ export class EndpointActionGenerator extends BaseDataGenerator { return super.randomN(max); } - protected randomIsolateCommand() { - return this.randomChoice(ISOLATION_COMMANDS); + protected randomResponseActionCommand() { + return this.randomChoice(RESPONSE_ACTION_COMMANDS); } } diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_action_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_action_generator.ts index 1ca380989e09e..f6558453648d2 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_action_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_action_generator.ts @@ -16,11 +16,9 @@ import { ActivityLogItemTypes, EndpointAction, EndpointActionResponse, - ISOLATION_ACTIONS, + RESPONSE_ACTION_COMMANDS, } from '../types'; -const ISOLATION_COMMANDS: ISOLATION_ACTIONS[] = ['isolate', 'unisolate']; - export class FleetActionGenerator extends BaseDataGenerator { /** Generate a random endpoint Action (isolate or unisolate) */ generate(overrides: DeepPartial = {}): EndpointAction { @@ -38,8 +36,9 @@ export class FleetActionGenerator extends BaseDataGenerator { agents: [this.seededUUIDv4()], user_id: 'elastic', data: { - command: this.randomIsolateCommand(), + command: this.randomResponseActionCommand(), comment: this.randomString(15), + parameter: undefined, }, }, overrides @@ -69,8 +68,9 @@ export class FleetActionGenerator extends BaseDataGenerator { return merge( { action_data: { - command: this.randomIsolateCommand(), + command: this.randomResponseActionCommand(), comment: '', + parameter: undefined, }, action_id: this.seededUUIDv4(), agent_id: this.seededUUIDv4(), @@ -135,7 +135,7 @@ export class FleetActionGenerator extends BaseDataGenerator { return super.randomN(max); } - protected randomIsolateCommand() { - return this.randomChoice(ISOLATION_COMMANDS); + protected randomResponseActionCommand() { + return this.randomChoice(RESPONSE_ACTION_COMMANDS); } } diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index 45865ad59a6c6..e5386013cd458 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -14,11 +14,15 @@ import { export type ISOLATION_ACTIONS = 'isolate' | 'unisolate'; -export type ResponseActions = - | ISOLATION_ACTIONS - | 'kill-process' - | 'suspend-process' - | 'running-processes'; +export const RESPONSE_ACTION_COMMANDS = [ + 'isolate', + 'unisolate', + 'kill-process', + 'suspend-process', + 'running-processes', +] as const; + +export type ResponseActions = typeof RESPONSE_ACTION_COMMANDS[number]; export const ActivityLogItemTypes = { ACTION: 'action' as const, @@ -232,7 +236,7 @@ export interface ActionDetails { * The Endpoint type of action (ex. `isolate`, `release`) that is being requested to be * performed on the endpoint */ - command: string; + command: ResponseActions; /** * Will be set to true only if action is not yet completed and elapsed time has exceeded * the request's expiration date @@ -248,10 +252,12 @@ export interface ActionDetails { startedAt: string; /** The date when the action was completed (a response by the endpoint (not fleet) was received) */ completedAt: string | undefined; - /** - * The list of action log items (actions and responses) received thus far for the action. - */ - logEntries: ActivityLogEntry[]; + /** user that created the action */ + createdBy: string; + /** comment submitted with action */ + comment?: string; + /** parameters submitted with action */ + parameters?: EndpointActionDataParameterTypes; } export interface ActionDetailsApiResponse { diff --git a/x-pack/plugins/security_solution/scripts/endpoint/action_responder/run_in_auto_mode.ts b/x-pack/plugins/security_solution/scripts/endpoint/action_responder/run_in_auto_mode.ts index a765eba2d062d..29b5f66786d08 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/action_responder/run_in_auto_mode.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/action_responder/run_in_auto_mode.ts @@ -126,19 +126,5 @@ const parseCommentTokens = (comment: string): CommentTokens => { }; const getActionComment = (action: ActionDetails): string => { - const actionRequest = action.logEntries.find( - (entry) => entry.type === 'fleetAction' || entry.type === 'action' - ); - - if (actionRequest) { - if (actionRequest.type === 'fleetAction') { - return actionRequest.item.data.data.comment ?? ''; - } - - if (actionRequest.type === 'action') { - return actionRequest.item.data.EndpointActions.data.comment ?? ''; - } - } - - return ''; + return action.comment ?? ''; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts index 5d024488d4958..ec8466b80f105 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts @@ -31,6 +31,7 @@ import { GET_RUNNING_PROCESSES_ROUTE, ISOLATE_HOST_ROUTE, UNISOLATE_HOST_ROUTE, + ENDPOINT_ACTIONS_INDEX, } from '../../../../common/endpoint/constants'; import type { EndpointAction, @@ -257,7 +258,7 @@ function responseActionRequestHandler( { - index: `${ENDPOINT_ACTIONS_DS}-default`, + index: ENDPOINT_ACTIONS_INDEX, body: { ...doc, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.test.ts index 32730fd4d4dbd..4f869bdd5e19b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.test.ts @@ -39,82 +39,20 @@ describe('When using `getActionDetailsById()', () => { }); it('should return expected output', async () => { + const doc = actionRequests.hits.hits[0]._source; await expect(getActionDetailsById(esClient, '123')).resolves.toEqual({ agents: ['agent-a'], - command: 'isolate', + command: 'unisolate', completedAt: '2022-04-30T16:08:47.449Z', wasSuccessful: true, errors: undefined, id: '123', isCompleted: true, isExpired: false, - logEntries: [ - { - item: { - data: { - '@timestamp': '2022-04-30T16:08:47.449Z', - EndpointActions: { - action_id: '123', - completed_at: '2022-04-30T16:08:47.449Z', - data: { - command: 'unisolate', - comment: '', - }, - started_at: expect.any(String), - }, - agent: { - id: 'agent-a', - }, - error: undefined, - }, - id: expect.any(String), - }, - type: 'response', - }, - { - item: { - data: { - '@timestamp': '2022-04-30T16:08:47.449Z', - action_data: { - command: 'unisolate', - comment: '', - }, - action_id: '123', - agent_id: 'agent-a', - completed_at: '2022-04-30T16:08:47.449Z', - error: '', - started_at: expect.any(String), - }, - id: expect.any(String), - }, - type: 'fleetResponse', - }, - { - item: { - data: { - '@timestamp': '2022-04-27T16:08:47.449Z', - EndpointActions: { - action_id: '123', - data: { - command: 'isolate', - comment: '5wb6pu6kh2xix5i', - }, - expiration: expect.any(String), - input_type: 'endpoint', - type: 'INPUT_ACTION', - }, - agent: { id: 'agent-a' }, - user: { - id: expect.any(String), - }, - error: undefined, - }, - id: expect.any(String), - }, - type: 'action', - }, - ], startedAt: '2022-04-27T16:08:47.449Z', + comment: doc?.EndpointActions.data.comment, + createdBy: doc?.user.id, + parameters: doc?.EndpointActions.data.parameters, }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.ts index bd1017866da87..ad6cf41976f01 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/action_details_by_id.ts @@ -23,7 +23,7 @@ import type { LogsEndpointAction, LogsEndpointActionResponse, } from '../../../../common/endpoint/types'; -import { catchAndWrapError, getTimeSortedActionListLogEntries } from '../../utils'; +import { catchAndWrapError } from '../../utils'; import { EndpointError } from '../../../../common/endpoint/errors'; import { NotFoundError } from '../../errors'; import { ACTION_RESPONSE_INDICES, ACTIONS_SEARCH_PAGE_SIZE } from './constants'; @@ -116,15 +116,14 @@ export const getActionDetailsById = async ( agents: normalizedActionRequest.agents, command: normalizedActionRequest.command, startedAt: normalizedActionRequest.createdAt, - logEntries: getTimeSortedActionListLogEntries([ - ...actionRequestsLogEntries, - ...actionResponses, - ]), isCompleted, completedAt, wasSuccessful, errors, isExpired: !isCompleted && normalizedActionRequest.expiration < new Date().toISOString(), + createdBy: normalizedActionRequest.createdBy, + comment: normalizedActionRequest.comment, + parameters: normalizedActionRequest.parameters, }; return actionDetails; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/action_list.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/action_list.test.ts index 048b24e723eac..52370377b3ee4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/action_list.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/action_list.test.ts @@ -42,6 +42,7 @@ describe('When using `getActionList()', () => { }); it('should return expected output', async () => { + const doc = actionRequests.hits.hits[0]._source; await expect(getActionList({ esClient, logger, page: 1, pageSize: 10 })).resolves.toEqual({ page: 0, pageSize: 10, @@ -53,80 +54,17 @@ describe('When using `getActionList()', () => { data: [ { agents: ['agent-a'], - command: 'isolate', + command: 'unisolate', completedAt: '2022-04-30T16:08:47.449Z', wasSuccessful: true, errors: undefined, id: '123', isCompleted: true, isExpired: false, - logEntries: [ - { - item: { - data: { - '@timestamp': '2022-04-30T16:08:47.449Z', - EndpointActions: { - action_id: '123', - completed_at: '2022-04-30T16:08:47.449Z', - data: { - command: 'unisolate', - comment: '', - }, - started_at: expect.any(String), - }, - agent: { - id: 'agent-a', - }, - error: undefined, - }, - id: expect.any(String), - }, - type: 'response', - }, - { - item: { - data: { - '@timestamp': '2022-04-30T16:08:47.449Z', - action_data: { - command: 'unisolate', - comment: '', - }, - action_id: '123', - agent_id: 'agent-a', - completed_at: '2022-04-30T16:08:47.449Z', - error: '', - started_at: expect.any(String), - }, - id: expect.any(String), - }, - type: 'fleetResponse', - }, - { - item: { - data: { - '@timestamp': '2022-04-27T16:08:47.449Z', - EndpointActions: { - action_id: '123', - data: { - command: 'isolate', - comment: '5wb6pu6kh2xix5i', - }, - expiration: expect.any(String), - input_type: 'endpoint', - type: 'INPUT_ACTION', - }, - agent: { id: 'agent-a' }, - user: { - id: expect.any(String), - }, - error: undefined, - }, - id: expect.any(String), - }, - type: 'action', - }, - ], startedAt: '2022-04-27T16:08:47.449Z', + comment: doc?.EndpointActions.data.comment, + createdBy: doc?.user.id, + parameters: doc?.EndpointActions.data.parameters, }, ], total: 1, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/action_list.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/action_list.ts index fccdb4e4adf50..2bbd44db7cc9d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/action_list.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/action_list.ts @@ -9,11 +9,7 @@ import { ElasticsearchClient, Logger } from '@kbn/core/server'; import { CustomHttpRequestError } from '../../../utils/custom_http_request_error'; import type { ActionDetails, ActionListApiResponse } from '../../../../common/endpoint/types'; -import { - getActions, - getActionResponses, - getTimeSortedActionListLogEntries, -} from '../../utils/action_list_helpers'; +import { getActions, getActionResponses } from '../../utils/action_list_helpers'; import { formatEndpointActionResults, @@ -157,10 +153,6 @@ const getActionDetailsList = async ({ // compute action details list for each action id const actionDetails: ActionDetails[] = normalizedActionRequests.map((action) => { - // pick only those actions that match the current action id - const matchedActions = formattedActionRequests.filter( - (categorizedAction) => categorizedAction.item.data.EndpointActions.action_id === action.id - ); // pick only those responses that match the current action id const matchedResponses = categorizedResponses.filter((categorizedResponse) => categorizedResponse.type === 'response' @@ -179,13 +171,14 @@ const getActionDetailsList = async ({ agents: action.agents, command: action.command, startedAt: action.createdAt, - // sort the list by @timestamp in desc order, newest first - logEntries: getTimeSortedActionListLogEntries([...matchedActions, ...matchedResponses]), isCompleted, completedAt, wasSuccessful, errors, isExpired: !isCompleted && action.expiration < new Date().toISOString(), + createdBy: action.createdBy, + comment: action.comment, + parameters: action.parameters, }; }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/utils.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/utils.test.ts index 469f85e2519a2..4e40a749f6a8a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/utils.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/utils.test.ts @@ -70,13 +70,14 @@ describe('When using Actions service utilities', () => { ) ).toEqual({ agents: ['6e6796b0-af39-4f12-b025-fcb06db499e5'], - command: 'isolate', + command: 'unisolate', comment: expect.any(String), createdAt: '2022-04-27T16:08:47.449Z', createdBy: 'elastic', expiration: '2022-04-29T16:08:47.449Z', id: '90d62689-f72d-4a05-b5e3-500cad0dc366', type: 'ACTION_REQUEST', + parameters: undefined, }); }); @@ -89,13 +90,14 @@ describe('When using Actions service utilities', () => { ) ).toEqual({ agents: ['90d62689-f72d-4a05-b5e3-500cad0dc366'], - command: 'isolate', + command: 'unisolate', comment: expect.any(String), createdAt: '2022-04-27T16:08:47.449Z', createdBy: 'Shanel', expiration: '2022-05-10T16:08:47.449Z', id: '1d6e6796-b0af-496f-92b0-25fcb06db499', type: 'ACTION_REQUEST', + parameters: undefined, }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/utils.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/utils.ts index 149bafe490f79..d50149e61c1c6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/utils.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/utils.ts @@ -16,11 +16,13 @@ import type { ActivityLogActionResponse, ActivityLogEntry, EndpointAction, + EndpointActionDataParameterTypes, EndpointActionResponse, EndpointActivityLogAction, EndpointActivityLogActionResponse, LogsEndpointAction, LogsEndpointActionResponse, + ResponseActions, } from '../../../../common/endpoint/types'; import { ActivityLogItemTypes } from '../../../../common/endpoint/types'; /** @@ -50,8 +52,9 @@ interface NormalizedActionRequest { agents: string[]; createdBy: string; createdAt: string; - command: string; + command: ResponseActions; comment?: string; + parameters?: EndpointActionDataParameterTypes; } /** @@ -76,6 +79,7 @@ export const mapToNormalizedActionRequest = ( expiration: actionRequest.EndpointActions.expiration, id: actionRequest.EndpointActions.action_id, type, + parameters: actionRequest.EndpointActions.data.parameters, }; } @@ -89,6 +93,7 @@ export const mapToNormalizedActionRequest = ( expiration: actionRequest.expiration, id: actionRequest.action_id, type, + parameters: actionRequest.data.parameters, }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/action_list_helpers.ts b/x-pack/plugins/security_solution/server/endpoint/utils/action_list_helpers.ts index 6cb752136dffc..1dd4c8ffca7be 100644 --- a/x-pack/plugins/security_solution/server/endpoint/utils/action_list_helpers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/utils/action_list_helpers.ts @@ -14,7 +14,6 @@ import type { TransportResult } from '@elastic/elasticsearch'; import { ENDPOINT_ACTIONS_INDEX } from '../../../common/endpoint/constants'; import type { LogsEndpointAction, - ActionListApiResponse, EndpointActionResponse, LogsEndpointActionResponse, } from '../../../common/endpoint/types'; @@ -27,17 +26,6 @@ const queryOptions = Object.freeze({ ignore: [404], }); -// This is same as the one for audit log -// but we want to deprecate audit log at some point -// thus creating this one for sorting action list log entries -export const getTimeSortedActionListLogEntries = ( - data: ActionListApiResponse['data'][number]['logEntries'] -): ActionListApiResponse['data'][number]['logEntries'] => { - return data.sort((a, b) => - new Date(b.item.data['@timestamp']) > new Date(a.item.data['@timestamp']) ? 1 : -1 - ); -}; - export const getActions = async ({ commands, elasticAgentIds, From 0dd2c35d185c6204b81701ed85b1958a59bf9658 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 23 Jun 2022 15:46:41 -0400 Subject: [PATCH 52/54] [Guided onboarding] Observability tour (#133909) --- .../routing/templates/apm_main_template.tsx | 9 +- .../infra/public/pages/logs/page_template.tsx | 1 + .../shared/page_template/page_template.tsx | 45 ++- .../public/components/shared/tour/index.ts | 8 + .../public/components/shared/tour/tour.scss | 3 + .../public/components/shared/tour/tour.tsx | 308 ++++++++++++++++++ .../containers/alerts_page/alerts_page.tsx | 1 + .../public/pages/cases/index.tsx | 1 + .../public/pages/overview/index.tsx | 2 + .../common/pages/synthetics_page_template.tsx | 1 + .../app/uptime_page_template.tsx | 1 + .../components/app/rum_dashboard/rum_home.tsx | 1 + x-pack/test/functional/apps/infra/index.ts | 1 + x-pack/test/functional/apps/infra/tour.ts | 126 +++++++ .../page_objects/infra_home_page.ts | 20 ++ 15 files changed, 511 insertions(+), 17 deletions(-) create mode 100644 x-pack/plugins/observability/public/components/shared/tour/index.ts create mode 100644 x-pack/plugins/observability/public/components/shared/tour/tour.scss create mode 100644 x-pack/plugins/observability/public/components/shared/tour/tour.tsx create mode 100644 x-pack/test/functional/apps/infra/tour.ts diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx index 777d7bcb67a96..55b486f5ae366 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx @@ -71,15 +71,17 @@ export function ApmMainTemplate({ [shouldBypassNoDataScreen, data?.hasData] ); + const isLoading = + status === FETCH_STATUS.LOADING || + fleetApmPoliciesStatus === FETCH_STATUS.LOADING; + const noDataConfig = getNoDataConfig({ basePath, docsLink: docLinks!.links.observability.guide, hasApmData: data?.hasData, hasApmIntegrations: fleetApmPoliciesData?.hasApmPolicies, shouldBypassNoDataScreen, - loading: - status === FETCH_STATUS.LOADING || - fleetApmPoliciesStatus === FETCH_STATUS.LOADING, + loading: isLoading, }); const { @@ -96,6 +98,7 @@ export function ApmMainTemplate({ const pageTemplate = ( = ({ ); diff --git a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx index 59c051f852ffe..949e83009019e 100644 --- a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx +++ b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx @@ -16,6 +16,7 @@ import { SharedUxServicesProvider } from '@kbn/shared-ux-services'; import type { SharedUXPluginStart } from '@kbn/shared-ux-plugin/public'; import { KibanaPageTemplate, KibanaPageTemplateProps } from '@kbn/shared-ux-components'; import type { NavigationSection } from '../../../services/navigation_registry'; +import { ObservabilityTour } from '../tour'; import { NavNameWithBadge, hideBadge } from './nav_name_with_badge'; export type WrappedPageTemplateProps = Pick< @@ -33,6 +34,7 @@ export type WrappedPageTemplateProps = Pick< | 'noDataConfig' > & { showSolutionNav?: boolean; + isPageDataLoaded?: boolean; }; export interface ObservabilityPageTemplateDependencies { @@ -54,6 +56,7 @@ export function ObservabilityPageTemplate({ navigationSections$, getSharedUXContext, showSolutionNav = true, + isPageDataLoaded = true, ...pageTemplateProps }: ObservabilityPageTemplateProps): React.ReactElement | null { const sections = useObservable(navigationSections$, []); @@ -90,6 +93,7 @@ export function ObservabilityPageTemplate({ ), href, isSelected, + 'data-nav-id': entry.label.toLowerCase().split(' ').join('_'), onClick: (event) => { if (entry.onClick) { entry.onClick(event); @@ -124,21 +128,34 @@ export function ObservabilityPageTemplate({ return ( - - {children} - + {({ isTourVisible }) => { + return ( + + {children} + + ); + }} + ); } diff --git a/x-pack/plugins/observability/public/components/shared/tour/index.ts b/x-pack/plugins/observability/public/components/shared/tour/index.ts new file mode 100644 index 0000000000000..a4f0e1e820180 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/tour/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 { ObservabilityTour } from './tour'; diff --git a/x-pack/plugins/observability/public/components/shared/tour/tour.scss b/x-pack/plugins/observability/public/components/shared/tour/tour.scss new file mode 100644 index 0000000000000..68d04250c5ec4 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/tour/tour.scss @@ -0,0 +1,3 @@ +.euiOverlayMask.observabilityTour__overlayMask { + left: 248px; +} diff --git a/x-pack/plugins/observability/public/components/shared/tour/tour.tsx b/x-pack/plugins/observability/public/components/shared/tour/tour.tsx new file mode 100644 index 0000000000000..f90668ea14139 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/tour/tour.tsx @@ -0,0 +1,308 @@ +/* + * 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 React, { ReactNode, useState, useCallback, useEffect } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiTourStep, + EuiTourStepProps, + EuiText, + ElementTarget, + EuiOverlayMask, + useIsWithinBreakpoints, +} from '@elastic/eui'; +import { useLocation } from 'react-router-dom'; +import { ApplicationStart } from '@kbn/core/public'; +import { observabilityAppId } from '../../../../common'; + +import './tour.scss'; + +interface TourStep { + content: EuiTourStepProps['content']; + anchor: ElementTarget; + anchorPosition: EuiTourStepProps['anchorPosition']; + title: EuiTourStepProps['title']; + dataTestSubj: string; + showOverlay: boolean; +} + +const minWidth: EuiTourStepProps['minWidth'] = 360; +const maxWidth: EuiTourStepProps['maxWidth'] = 360; +const offset: EuiTourStepProps['offset'] = 30; +const repositionOnScroll: EuiTourStepProps['repositionOnScroll'] = false; + +const overviewPath = '/overview'; +const guidedSetupStep = 6; + +const observabilityTourStorageKey = 'xpack.observability.tourState'; + +const tourStepsConfig: TourStep[] = [ + { + title: i18n.translate('xpack.observability.tour.observabilityOverviewStep.tourTitle', { + defaultMessage: 'Welcome to Elastic Observability', + }), + content: ( + + {i18n.translate('xpack.observability.tour.observabilityOverviewStep.tourContent', { + defaultMessage: + 'Take a quick tour of the Observability solution to get a feel for how it works.', + })} + + ), + anchor: `[id^="KibanaPageTemplateSolutionNav"]`, + anchorPosition: 'rightUp', + dataTestSubj: 'overviewStep', + showOverlay: true, + }, + { + title: i18n.translate('xpack.observability.tour.streamStep.tourTitle', { + defaultMessage: 'View all your infrastructure logs in real time', + }), + content: ( + + {i18n.translate('xpack.observability.tour.streamStep.tourContent', { + defaultMessage: 'Verify your data is flowing correctly.', + })} + + ), + anchor: `[data-nav-id="stream"]`, + anchorPosition: 'rightUp', + dataTestSubj: 'streamStep', + showOverlay: true, + }, + { + title: i18n.translate('xpack.observability.tour.metricsExplorerStep.tourTitle', { + defaultMessage: 'Inspect your overall infrastructure performance', + }), + content: ( + + {i18n.translate('xpack.observability.tour.metricsExplorerStep.tourContent', { + defaultMessage: 'Check the health of your infrastructure.', + })} + + ), + anchor: `[data-nav-id="metrics_explorer"]`, + anchorPosition: 'rightUp', + dataTestSubj: 'metricsExplorerStep', + showOverlay: true, + }, + { + title: i18n.translate('xpack.observability.tour.tracesStep.tourTitle', { + defaultMessage: 'Understand the entire lifecycle of a request/action', + }), + content: ( + + {i18n.translate('xpack.observability.tour.tracesStep.tourContent', { + defaultMessage: 'Track down any issues affecting your infrastructure.', + })} + + ), + anchor: `[data-nav-id="traces"]`, + anchorPosition: 'rightUp', + dataTestSubj: 'tracesStep', + showOverlay: true, + }, + { + title: i18n.translate('xpack.observability.tour.alertsStep.tourTitle', { + defaultMessage: 'Get notified when something goes wrong', + }), + content: ( + + {i18n.translate('xpack.observability.tour.alertsStep.tourContent', { + defaultMessage: 'Configure how you want to be notified when a problem occurs.', + })} + + ), + anchor: `[data-nav-id="alerts"]`, + anchorPosition: 'rightUp', + dataTestSubj: 'alertStep', + showOverlay: true, + }, + { + title: i18n.translate('xpack.observability.tour.guidedSetupStep.tourTitle', { + defaultMessage: `You're ready!`, + }), + content: ( + + {i18n.translate('xpack.observability.tour.guidedSetupStep.tourContent', { + defaultMessage: 'View the guided setup to learn about next steps.', + })} + + ), + anchor: '#guidedSetupButton', + anchorPosition: 'rightUp', + dataTestSubj: 'guidedSetupStep', + showOverlay: false, + }, +]; + +const getSteps = ({ + activeStep, + incrementStep, + endTour, +}: { + activeStep: number; + incrementStep: () => void; + endTour: () => void; +}) => { + const footerAction = ( + + + endTour()} + size="xs" + color="text" + data-test-subj="skipButton" + > + {i18n.translate('xpack.observability.tour.skipButtonLabel', { + defaultMessage: 'Skip', + })} + + + + incrementStep()} + size="s" + color="success" + data-test-subj="nextButton" + > + {i18n.translate('xpack.observability.tour.nextButtonLabel', { + defaultMessage: 'Next', + })} + + + + ); + + const lastStepFooterAction = ( + endTour()} data-test-subj="endButton"> + {i18n.translate('xpack.observability.tour.endButtonLabel', { + defaultMessage: 'End tour', + })} + + ); + + return tourStepsConfig.map((stepConfig, index) => { + const step = index + 1; + const { dataTestSubj, showOverlay, ...tourStepProps } = stepConfig; + return ( + endTour()} + footerAction={activeStep === tourStepsConfig.length ? lastStepFooterAction : footerAction} + panelProps={{ + 'data-test-subj': dataTestSubj, + }} + /> + ); + }); +}; + +interface TourState { + activeStep: number; + isTourActive: boolean; +} + +const getInitialTourState = (prevTourState: string | null): TourState => { + if (prevTourState) { + try { + const parsedPrevTourState = JSON.parse(prevTourState); + return parsedPrevTourState as TourState; + } catch (e) { + // Fall back to default state + } + } + + return { + activeStep: 1, + isTourActive: false, + }; +}; + +export function ObservabilityTour({ + children, + navigateToApp, + isPageDataLoaded, + showTour, +}: { + children: ({ isTourVisible }: { isTourVisible: boolean }) => ReactNode; + navigateToApp: ApplicationStart['navigateToApp']; + isPageDataLoaded: boolean; + showTour: boolean; +}) { + const prevTourState = localStorage.getItem(observabilityTourStorageKey); + const { activeStep: initialActiveStep, isTourActive: initialIsTourActive } = + getInitialTourState(prevTourState); + + const [isTourActive, setIsTourActive] = useState(initialIsTourActive); + const [activeStep, setActiveStep] = useState(initialActiveStep); + + const { pathname: currentPath } = useLocation(); + + const isSmallBreakpoint = useIsWithinBreakpoints(['s']); + + const isOverviewPage = currentPath === overviewPath; + const { showOverlay } = tourStepsConfig[activeStep - 1]; + + const incrementStep = useCallback(() => { + setActiveStep((prevState) => prevState + 1); + }, []); + + const endTour = useCallback(() => setIsTourActive(false), []); + + /** + * The tour should only be visible if the following conditions are met: + * - Only pages with the side nav should show the tour (showTour === true) + * - Tour is set to active per localStorage setting (isTourActive === true) + * - Any page data must be loaded in order for the tour to render correctly + * - The tour should only render on medium-large screens + */ + const isTourVisible = showTour && isTourActive && isPageDataLoaded && isSmallBreakpoint === false; + + useEffect(() => { + localStorage.setItem(observabilityTourStorageKey, JSON.stringify({ isTourActive, activeStep })); + }, [isTourActive, activeStep]); + + useEffect(() => { + // The user must be on the overview page to view the guided setup step in the tour + if (isTourActive && isOverviewPage === false && activeStep === guidedSetupStep) { + navigateToApp(observabilityAppId, { + path: overviewPath, + }); + } + }, [activeStep, isOverviewPage, isTourActive, navigateToApp]); + + return ( + <> + {children({ isTourVisible })} + {isTourVisible && ( + <> + {getSteps({ activeStep, incrementStep, endTour })} + {showOverlay && ( + + )} + + )} + + ); +} diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx index a39e805941fe3..0ead6468ad0b0 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx @@ -234,6 +234,7 @@ function AlertsPage() { return ( { return userPermissions == null || userPermissions?.read ? ( diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx index 42f586a13d1e1..19f855b760cd4 100644 --- a/x-pack/plugins/observability/public/pages/overview/index.tsx +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -160,6 +160,7 @@ export function OverviewPage({ routeParams }: Props) { return ( {showLoading && } diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.tsx index fa3ad7e0805e8..e6770dcb075ed 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.tsx @@ -76,6 +76,7 @@ export const UptimePageTemplateComponent: React.FC pageHeader={pageHeader} data-test-subj={noDataConfig ? 'data-missing' : undefined} noDataConfig={isMainRoute && !loading ? noDataConfig : undefined} + isPageDataLoaded={Boolean(loading === false && isMainRoute && data)} {...pageTemplateProps} > {showLoading && } diff --git a/x-pack/plugins/ux/public/components/app/rum_dashboard/rum_home.tsx b/x-pack/plugins/ux/public/components/app/rum_dashboard/rum_home.tsx index aafaf22a9bba3..a6242d0617f0f 100644 --- a/x-pack/plugins/ux/public/components/app/rum_dashboard/rum_home.tsx +++ b/x-pack/plugins/ux/public/components/app/rum_dashboard/rum_home.tsx @@ -62,6 +62,7 @@ export function RumHome() { }} + isPageDataLoaded={isLoading === false} > {isLoading && }
diff --git a/x-pack/test/functional/apps/infra/index.ts b/x-pack/test/functional/apps/infra/index.ts index d574c747bf041..9b6e33da3178d 100644 --- a/x-pack/test/functional/apps/infra/index.ts +++ b/x-pack/test/functional/apps/infra/index.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('InfraOps App', function () { loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./tour')); describe('Metrics UI', function () { loadTestFile(require.resolve('./home_page')); diff --git a/x-pack/test/functional/apps/infra/tour.ts b/x-pack/test/functional/apps/infra/tour.ts new file mode 100644 index 0000000000000..9806115c6129f --- /dev/null +++ b/x-pack/test/functional/apps/infra/tour.ts @@ -0,0 +1,126 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + const pageObjects = getPageObjects(['common', 'infraHome']); + const find = getService('find'); + + const setInitialTourState = async (activeStep?: number) => { + await browser.setLocalStorageItem( + 'xpack.observability.tourState', + JSON.stringify({ + activeStep: activeStep || 1, + isTourActive: true, + }) + ); + await browser.refresh(); + }; + + describe('Onboarding Observability tour', function () { + this.tags('includeFirefox'); + + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); + await pageObjects.common.navigateToApp('observability'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs'); + await browser.removeLocalStorageItem('xpack.observability.tourState'); + }); + + describe('Tour enabled', () => { + it('can complete tour', async () => { + await setInitialTourState(); + + // Step 1: Overview + await pageObjects.infraHome.waitForTourStep('overviewStep'); + await pageObjects.infraHome.clickTourNextButton(); + + // Step 2: Streams + await pageObjects.infraHome.waitForTourStep('streamStep'); + await pageObjects.infraHome.clickTourNextButton(); + + // Step 3: Metrics explorer + await pageObjects.infraHome.waitForTourStep('metricsExplorerStep'); + await pageObjects.infraHome.clickTourNextButton(); + + // Step 4: Traces + await pageObjects.infraHome.waitForTourStep('tracesStep'); + await pageObjects.infraHome.clickTourNextButton(); + + // Step 5: Alerts + await pageObjects.infraHome.waitForTourStep('alertStep'); + await pageObjects.infraHome.clickTourNextButton(); + + // Step 6: Guided setup + await pageObjects.infraHome.waitForTourStep('guidedSetupStep'); + await pageObjects.infraHome.clickTourEndButton(); + await pageObjects.infraHome.ensureTourStepIsClosed('guidedSetupStep'); + }); + + it('can skip tour', async () => { + await setInitialTourState(); + + await pageObjects.infraHome.waitForTourStep('overviewStep'); + await pageObjects.infraHome.clickTourSkipButton(); + + // Verify current step ("Overview") is not displayed + await pageObjects.infraHome.ensureTourStepIsClosed('overviewStep'); + // Verify next step ("Streams") is not displayed + await pageObjects.infraHome.ensureTourStepIsClosed('streamStep'); + + await browser.refresh(); + + // Verify current step ("Overview") is not displayed after browser refresh, + // i.e., localStorage has been updated to not show the tour again + await pageObjects.infraHome.ensureTourStepIsClosed('overviewStep'); + }); + + it('can start mid-tour', async () => { + await setInitialTourState(5); + + // Step 5: Alerts + await pageObjects.infraHome.waitForTourStep('alertStep'); + await pageObjects.infraHome.clickTourNextButton(); + + // Step 6: Guided setup + await pageObjects.infraHome.waitForTourStep('guidedSetupStep'); + await pageObjects.infraHome.clickTourEndButton(); + await pageObjects.infraHome.ensureTourStepIsClosed('guidedSetupStep'); + }); + + it('navigates the user to the guided setup step', async () => { + // For brevity, starting the tour at step 5 + await setInitialTourState(5); + + await pageObjects.infraHome.waitForTourStep('alertStep'); + + // Click on Alerts link + await (await find.byCssSelector('[data-nav-id="alerts"]')).click(); + + // Verify user correctly navigated to the Alerts page + const alertsPageUrl = await browser.getCurrentUrl(); + expect(alertsPageUrl).to.contain('/app/observability/alerts'); + + // Verify Step 5 persists on Alerts page, then continue with tour + await pageObjects.infraHome.waitForTourStep('alertStep'); + await pageObjects.infraHome.clickTourNextButton(); + + // Verify user navigated back to the overview page, and guided setup step renders (Step 6) + await pageObjects.infraHome.waitForTourStep('guidedSetupStep'); + const overviewPageUrl = await browser.getCurrentUrl(); + expect(overviewPageUrl).to.contain('/app/observability/overview'); + }); + }); + }); +}; diff --git a/x-pack/test/functional/page_objects/infra_home_page.ts b/x-pack/test/functional/page_objects/infra_home_page.ts index 5a3cb70757b0d..10c2c8237aedc 100644 --- a/x-pack/test/functional/page_objects/infra_home_page.ts +++ b/x-pack/test/functional/page_objects/infra_home_page.ts @@ -328,5 +328,25 @@ export function InfraHomePageProvider({ getService, getPageObjects }: FtrProvide async closeAlertFlyout() { await testSubjects.click('euiFlyoutCloseButton'); }, + + async waitForTourStep(tourStep: string) { + await retry.waitFor('tour step', () => testSubjects.exists(tourStep)); + }, + + async ensureTourStepIsClosed(tourStep: string) { + await testSubjects.missingOrFail(tourStep); + }, + + async clickTourNextButton() { + await testSubjects.click('nextButton'); + }, + + async clickTourEndButton() { + await testSubjects.click('endButton'); + }, + + async clickTourSkipButton() { + await testSubjects.click('skipButton'); + }, }; } From 691d4b68bd3b11334335cf4b3ca43e4d11bdb193 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Thu, 23 Jun 2022 22:54:48 +0300 Subject: [PATCH 53/54] [XY] Wrong visType for horizontal_bar visualizaiton (#135013) --- .../vis_types/xy/public/expression_functions/xy_vis_fn.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/vis_types/xy/public/expression_functions/xy_vis_fn.ts b/src/plugins/vis_types/xy/public/expression_functions/xy_vis_fn.ts index 08319e8e9a11b..8dc7481300bc0 100644 --- a/src/plugins/vis_types/xy/public/expression_functions/xy_vis_fn.ts +++ b/src/plugins/vis_types/xy/public/expression_functions/xy_vis_fn.ts @@ -19,13 +19,13 @@ import { DEFAULT_LEGEND_SIZE, LegendSize, } from '@kbn/visualizations-plugin/public'; -import type { ChartType } from '../../common'; import type { VisParams, XYVisConfig } from '../types'; +import type { XyVisType } from '../../common'; export const visName = 'xy_vis'; export interface RenderValue { visData: Datatable; - visType: ChartType; + visType: XyVisType; visConfig: VisParams; syncColors: boolean; syncTooltips: boolean; @@ -262,7 +262,7 @@ export const visTypeXyVisFn = (): VisTypeXyExpressionFunctionDefinition => ({ }, }, fn(context, args, handlers) { - const visType = args.chartType; + const visType = args.type; const visConfig = { ariaLabel: args.ariaLabel ?? From 48db1ec0f00df3207e72d861d7a71608de63ecac Mon Sep 17 00:00:00 2001 From: Brandon Morelli Date: Thu, 23 Jun 2022 14:08:51 -0600 Subject: [PATCH 54/54] docs: add advanced options (#134960) --- docs/management/advanced-options.asciidoc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 120617ea4de94..5ed2d734a81c2 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -374,9 +374,21 @@ value is 10000. [[apm-enable-service-overview]]`apm:enableServiceOverview`:: When enabled, displays the *Overview* tab for services in *APM*. +[[observability-apm-optimized-sort]]`observability:apmServiceInventoryOptimizedSorting`:: +preview:[] Sorts services without anomaly detection rules on the APM Service inventory page by service name. + +[[observability-apm-enable-comparison]]`observability:enableComparisonByDefault`:: +Enables the comparison feature in the APM app. + +[[observability-apm-enable-infra-view]]`observability:enableInfrastructureView`:: +Enables the Infrastructure view in the APM app. + [[observability-enable-inspect-es-queries]]`observability:enableInspectEsQueries`:: When enabled, allows you to inspect {es} queries in API responses. +[[observability-apm-enable-service-groups]]`observability:enableServiceGroups`:: +preview:[] When enabled, allows users to create Service Groups from the APM Service Inventory page. + [float] [[kibana-reporting-settings]] ==== Reporting