From dc13a02de7c00db4ae4574775690012cdaa96595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 19 Sep 2024 08:19:22 +0100 Subject: [PATCH 01/19] [8.x] [Spaces] Dynamically set the space disabled feature based on the space solution view (#191927) (#193299) --- .../capabilities_switcher.test.ts | 82 +++++++++++++++++ .../capabilities/capabilities_switcher.ts | 7 +- .../on_post_auth_interceptor.ts | 14 ++- .../space_solution_disabled_features.test.ts | 89 +++++++++++++++++++ .../utils/space_solution_disabled_features.ts | 64 +++++++++++++ 5 files changed, 251 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.test.ts create mode 100644 x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.ts diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts index b7bb839a752c6..31df41beae3cf 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts @@ -20,6 +20,8 @@ const features = [ id: 'feature_1', name: 'Feature 1', app: [], + category: { id: 'enterpriseSearch' }, + scope: ['spaces', 'security'], }, { id: 'feature_2', @@ -39,6 +41,7 @@ const features = [ }, }, }, + category: { id: 'observability' }, }, { id: 'feature_3', @@ -58,6 +61,7 @@ const features = [ }, }, }, + category: { id: 'securitySolution' }, }, { // feature 4 intentionally delcares the same items as feature 3 @@ -78,6 +82,7 @@ const features = [ }, }, }, + category: { id: 'observability' }, }, ] as unknown as KibanaFeature[]; @@ -317,4 +322,81 @@ describe('capabilitiesSwitcher', () => { expect(result).toEqual(expectedCapabilities); }); + + describe('when the space has a solution set', () => { + it('does toggles capabilities of the solutions different from the space one even when the space has no disabled features', async () => { + const space: Space = { + id: 'space', + name: '', + disabledFeatures: [], + }; + + const capabilities = buildCapabilities(); + + const { switcher } = setup(space); + const request = httpServerMock.createKibanaRequest(); + + { + space.solution = 'es'; + + // It should disable observability and securitySolution features + // which correspond to feature_2 and feature_3 + const result = await switcher(request, capabilities, false); + + const expectedCapabilities = buildCapabilities(); + + expectedCapabilities.navLinks.feature2 = false; + expectedCapabilities.catalogue.feature2Entry = false; + expectedCapabilities.navLinks.feature3 = false; + expectedCapabilities.catalogue.feature3Entry = false; + expectedCapabilities.navLinks.feature3_app = false; + expectedCapabilities.management.kibana.indices = false; + expectedCapabilities.management.kibana.somethingElse = false; + expectedCapabilities.feature_2.bar = false; + expectedCapabilities.feature_2.foo = false; + expectedCapabilities.feature_3.bar = false; + expectedCapabilities.feature_3.foo = false; + + expect(result).toEqual(expectedCapabilities); + } + + { + space.solution = 'oblt'; + + // It should disable enterpriseSearch and securitySolution features + // which correspond to feature_1 and feature_3 + const result = await switcher(request, capabilities, false); + + const expectedCapabilities = buildCapabilities(); + + expectedCapabilities.feature_1.bar = false; + expectedCapabilities.feature_1.foo = false; + expectedCapabilities.feature_3.bar = false; + expectedCapabilities.feature_3.foo = false; + + expect(result).toEqual(expectedCapabilities); + } + + { + space.solution = 'security'; + + // It should disable enterpriseSearch and observability features + // which correspond to feature_1 and feature_2 + const result = await switcher(request, capabilities, false); + + const expectedCapabilities = buildCapabilities(); + + expectedCapabilities.navLinks.feature2 = false; + expectedCapabilities.catalogue.feature2Entry = false; + expectedCapabilities.navLinks.feature3 = false; + expectedCapabilities.management.kibana.somethingElse = false; + expectedCapabilities.feature_1.bar = false; + expectedCapabilities.feature_1.foo = false; + expectedCapabilities.feature_2.bar = false; + expectedCapabilities.feature_2.foo = false; + + expect(result).toEqual(expectedCapabilities); + } + }); + }); }); diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts index d37337852b6b9..90ee85fece486 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts @@ -11,6 +11,7 @@ import type { Capabilities, CapabilitiesSwitcher, CoreSetup, Logger } from '@kbn import type { KibanaFeature } from '@kbn/features-plugin/server'; import type { Space } from '../../common'; +import { withSpaceSolutionDisabledFeatures } from '../lib/utils/space_solution_disabled_features'; import type { PluginsStart } from '../plugin'; import type { SpacesServiceStart } from '../spaces_service'; @@ -61,7 +62,11 @@ function toggleDisabledFeatures( capabilities: Capabilities, activeSpace: Space ) { - const disabledFeatureKeys = activeSpace.disabledFeatures; + const disabledFeatureKeys = withSpaceSolutionDisabledFeatures( + features, + activeSpace.disabledFeatures, + activeSpace.solution + ); const { enabledFeatures, disabledFeatures } = features.reduce( (acc, feature) => { diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts index 99f0bc8f1e5ce..67617185ad0f2 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts @@ -14,6 +14,7 @@ import type { PluginsSetup } from '../../plugin'; import type { SpacesServiceStart } from '../../spaces_service/spaces_service'; import { wrapError } from '../errors'; import { getSpaceSelectorUrl } from '../get_space_selector_url'; +import { withSpaceSolutionDisabledFeatures } from '../utils/space_solution_disabled_features'; export interface OnPostAuthInterceptorDeps { http: CoreSetup['http']; @@ -105,18 +106,23 @@ export function initSpacesOnPostAuthRequestInterceptor({ } } + const allFeatures = features.getKibanaFeatures(); + const disabledFeatureKeys = withSpaceSolutionDisabledFeatures( + allFeatures, + space.disabledFeatures, + space.solution + ); + // Verify application is available in this space // The management page is always visible, so we shouldn't be restricting access to the kibana application in any situation. const appId = path.split('/', 3)[2]; - if (appId !== 'kibana' && space && space.disabledFeatures.length > 0) { + if (appId !== 'kibana' && space && disabledFeatureKeys.length > 0) { log.debug(`Verifying application is available: "${appId}"`); - const allFeatures = features.getKibanaFeatures(); - const isRegisteredApp = allFeatures.some((feature) => feature.app.includes(appId)); if (isRegisteredApp) { const enabledFeatures = allFeatures.filter( - (feature) => !space.disabledFeatures.includes(feature.id) + (feature) => !disabledFeatureKeys.includes(feature.id) ); const isAvailableInSpace = enabledFeatures.some((feature) => feature.app.includes(appId)); diff --git a/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.test.ts b/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.test.ts new file mode 100644 index 0000000000000..908a4ee2ced57 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.test.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { KibanaFeature } from '@kbn/features-plugin/server'; + +import { withSpaceSolutionDisabledFeatures } from './space_solution_disabled_features'; + +const features = [ + { id: 'feature1', category: { id: 'observability' } }, + { id: 'feature2', category: { id: 'enterpriseSearch' } }, + { id: 'feature3', category: { id: 'securitySolution' } }, + { id: 'feature4', category: { id: 'should_not_be_returned' } }, // not a solution, it should never appeared in the disabled features +] as KibanaFeature[]; + +describe('#withSpaceSolutionDisabledFeatures', () => { + describe('when the space solution is not set (undefined)', () => { + test('it does not remove any features', () => { + const spaceDisabledFeatures: string[] = ['foo']; + + const result = withSpaceSolutionDisabledFeatures(features, spaceDisabledFeatures); + + expect(result).toEqual(['foo']); + }); + }); + + describe('when the space solution is "classic"', () => { + test('it does not remove any features', () => { + const spaceDisabledFeatures: string[] = ['foo']; + const spaceSolution = 'classic'; + + const result = withSpaceSolutionDisabledFeatures( + features, + spaceDisabledFeatures, + spaceSolution + ); + + expect(result).toEqual(['foo']); + }); + }); + + describe('when the space solution is "es"', () => { + test('it removes the "oblt" and "security" features', () => { + const spaceDisabledFeatures: string[] = ['foo']; + const spaceSolution = 'es'; + + const result = withSpaceSolutionDisabledFeatures( + features, + spaceDisabledFeatures, + spaceSolution + ); + + // merges the spaceDisabledFeatures with the disabledFeatureKeysFromSolution + expect(result).toEqual(['feature1', 'feature3']); // "foo" from the spaceDisabledFeatures should not be removed + }); + }); + + describe('when the space solution is "oblt"', () => { + test('it removes the "search" and "security" features', () => { + const spaceDisabledFeatures: string[] = []; + const spaceSolution = 'oblt'; + + const result = withSpaceSolutionDisabledFeatures( + features, + spaceDisabledFeatures, + spaceSolution + ); + + expect(result).toEqual(['feature2', 'feature3']); + }); + }); + + describe('when the space solution is "security"', () => { + test('it removes the "observability" and "enterpriseSearch" features', () => { + const spaceDisabledFeatures: string[] = ['baz']; + const spaceSolution = 'security'; + + const result = withSpaceSolutionDisabledFeatures( + features, + spaceDisabledFeatures, + spaceSolution + ); + + expect(result).toEqual(['feature1', 'feature2']); // "baz" from the spaceDisabledFeatures should not be removed + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.ts b/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.ts new file mode 100644 index 0000000000000..4e66260f3d057 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/utils/space_solution_disabled_features.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { KibanaFeature } from '@kbn/features-plugin/server'; + +import type { SolutionView } from '../../../common'; + +const getFeatureIdsForCategories = ( + features: KibanaFeature[], + categories: Array<'observability' | 'enterpriseSearch' | 'securitySolution'> +) => { + return features + .filter((feature) => + feature.category + ? categories.includes( + feature.category.id as 'observability' | 'enterpriseSearch' | 'securitySolution' + ) + : false + ) + .map((feature) => feature.id); +}; + +/** + * When a space has a `solution` defined, we want to disable features that are not part of that solution. + * This function takes the current space's disabled features and the space solution and returns + * the updated array of disabled features. + * + * @param spaceDisabledFeatures The current space's disabled features + * @param spaceSolution The current space's solution (es, oblt, security or classic) + * @returns The updated array of disabled features + */ +export function withSpaceSolutionDisabledFeatures( + features: KibanaFeature[], + spaceDisabledFeatures: string[] = [], + spaceSolution: SolutionView = 'classic' +): string[] { + if (spaceSolution === 'classic') { + return spaceDisabledFeatures; + } + + let disabledFeatureKeysFromSolution: string[] = []; + + if (spaceSolution === 'es') { + disabledFeatureKeysFromSolution = getFeatureIdsForCategories(features, [ + 'observability', + 'securitySolution', + ]); + } else if (spaceSolution === 'oblt') { + disabledFeatureKeysFromSolution = getFeatureIdsForCategories(features, [ + 'enterpriseSearch', + 'securitySolution', + ]); + } else if (spaceSolution === 'security') { + disabledFeatureKeysFromSolution = getFeatureIdsForCategories(features, [ + 'observability', + 'enterpriseSearch', + ]); + } + + return Array.from(new Set([...disabledFeatureKeysFromSolution])); +} From 34546af47b405b83be9aa51073aba59c00717d86 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 19 Sep 2024 19:16:41 +1000 Subject: [PATCH 02/19] [8.x] [ci] skip FTRs that fail on chrome 129 (#193293) (#193392) # Backport This will backport the following commits from `main` to `8.x`: - [[ci] skip FTRs that fail on chrome 129 (#193293)](https://github.com/elastic/kibana/pull/193293) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Alex Szabo --- test/functional/apps/dashboard/group1/url_field_formatter.ts | 3 ++- test/functional/apps/dashboard/group5/embed_mode.ts | 4 ++-- test/functional/apps/discover/group6/_time_field_column.ts | 3 ++- x-pack/test/functional/apps/lens/group6/workspace_size.ts | 5 +++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/test/functional/apps/dashboard/group1/url_field_formatter.ts b/test/functional/apps/dashboard/group1/url_field_formatter.ts index 50edce184a04d..f0282eff4ea13 100644 --- a/test/functional/apps/dashboard/group1/url_field_formatter.ts +++ b/test/functional/apps/dashboard/group1/url_field_formatter.ts @@ -37,7 +37,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(currentUrl).to.equal(fieldUrl); }; - describe('Changing field formatter to Url', () => { + // Fails in chrome 129+: https://github.com/elastic/kibana-operations/issues/199 + describe.skip('Changing field formatter to Url', () => { before(async function () { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader', 'animals']); await kibanaServer.savedObjects.cleanStandardList(); diff --git a/test/functional/apps/dashboard/group5/embed_mode.ts b/test/functional/apps/dashboard/group5/embed_mode.ts index 5bba11d3b574a..f2f64d76bce10 100644 --- a/test/functional/apps/dashboard/group5/embed_mode.ts +++ b/test/functional/apps/dashboard/group5/embed_mode.ts @@ -57,7 +57,7 @@ export default function ({ await browser.setWindowSize(1300, 900); }); - // Fails in with chrome 128+ https://github.com/elastic/kibana/issues/163207 + // Fails in chrome 128+ https://github.com/elastic/kibana-operations/issues/199 describe.skip('default URL params', () => { it('hides the chrome', async () => { const globalNavShown = await globalNav.exists(); @@ -92,7 +92,7 @@ export default function ({ }); }); - // Fails in with chrome 128+ https://github.com/elastic/kibana/issues/163207 + // Fails in chrome 128+ https://github.com/elastic/kibana-operations/issues/199 describe.skip('non-default URL params', () => { it('shows or hides elements based on URL params', async () => { const currentUrl = await browser.getCurrentUrl(); diff --git a/test/functional/apps/discover/group6/_time_field_column.ts b/test/functional/apps/discover/group6/_time_field_column.ts index f8279d532e6c4..7e058a71eac0b 100644 --- a/test/functional/apps/discover/group6/_time_field_column.ts +++ b/test/functional/apps/discover/group6/_time_field_column.ts @@ -308,7 +308,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - it('should render selected columns correctly', async () => { + // Fails in chrome 129+: https://github.com/elastic/kibana-operations/issues/199 + it.skip('should render selected columns correctly', async () => { await discover.selectTextBaseLang(); await checkSelectedColumns({ diff --git a/x-pack/test/functional/apps/lens/group6/workspace_size.ts b/x-pack/test/functional/apps/lens/group6/workspace_size.ts index 9c5e7fec773f0..0ba44a5249c6e 100644 --- a/x-pack/test/functional/apps/lens/group6/workspace_size.ts +++ b/x-pack/test/functional/apps/lens/group6/workspace_size.ts @@ -268,14 +268,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await assertWorkspaceDimensions('600px', '375px'); }); - // Fails in chrome 128+ + // Fails in chrome 128+: https://github.com/elastic/kibana-operations/issues/199 it.skip('gauge size (absolute pixels) - major arc', async () => { await lens.openVisualOptions(); await lens.setGaugeShape('Major arc'); await assertWorkspaceDimensions('600px', '430px'); }); - it('gauge size (absolute pixels) - circle', async () => { + // Fails in chrome 129+: https://github.com/elastic/kibana-operations/issues/199 + it.skip('gauge size (absolute pixels) - circle', async () => { await lens.openVisualOptions(); await lens.setGaugeShape('Circle'); await assertWorkspaceDimensions('600px', '430px'); From 99af35627df26cdbe11856e06657800f7c996cc8 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 19 Sep 2024 19:50:19 +1000 Subject: [PATCH 03/19] [8.x] [Cloud Security] Host Name Misconfiguration Datagrid & Refactor CSP Plugin PHASE 2 (#192535) (#192942) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [[Cloud Security] Host Name Misconfiguration Datagrid & Refactor CSP Plugin PHASE 2 (#192535)](https://github.com/elastic/kibana/pull/192535) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Rickyanto Ang --- .../index.ts | 2 +- .../types/findings.ts | 2 +- .../hooks/use_misconfiguration_findings.ts | 60 ++++++ .../src/hooks/use_misconfiguration_preview.ts | 124 ++---------- .../src/utils/hooks_utils.ts | 105 ++++++++++ .../kbn-cloud-security-posture/type.ts | 27 +++ .../csp_details/insights_tab_csp.tsx | 65 +++++++ ...isconfiguration_findings_details_table.tsx | 184 ++++++++++++++++++ .../misconfiguration_preview.tsx | 60 +++++- .../entity_details_flyout/index.tsx | 20 ++ .../host_details_left/index.test.tsx | 35 +++- .../host_details_left/index.tsx | 46 +++-- .../entity_details/host_right/index.tsx | 35 +++- .../left_panel/left_panel_header.tsx | 5 +- 14 files changed, 636 insertions(+), 134 deletions(-) create mode 100644 x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_findings.ts create mode 100644 x-pack/packages/kbn-cloud-security-posture/src/utils/hooks_utils.ts create mode 100644 x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/insights_tab_csp.tsx create mode 100644 x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/misconfiguration_findings_details_table.tsx diff --git a/x-pack/packages/kbn-cloud-security-posture-common/index.ts b/x-pack/packages/kbn-cloud-security-posture-common/index.ts index b5211af3342a3..86ed573fc3915 100644 --- a/x-pack/packages/kbn-cloud-security-posture-common/index.ts +++ b/x-pack/packages/kbn-cloud-security-posture-common/index.ts @@ -17,7 +17,7 @@ export type { BaseCspSetupStatus, CspSetupStatus, } from './types/status'; -export type { CspFinding } from './types/findings'; +export type { CspFinding, CspFindingResult } from './types/findings'; export type { BenchmarksCisId } from './types/benchmark'; export * from './constants'; export { diff --git a/x-pack/packages/kbn-cloud-security-posture-common/types/findings.ts b/x-pack/packages/kbn-cloud-security-posture-common/types/findings.ts index 19b0d685f2347..52503b589c44a 100644 --- a/x-pack/packages/kbn-cloud-security-posture-common/types/findings.ts +++ b/x-pack/packages/kbn-cloud-security-posture-common/types/findings.ts @@ -41,7 +41,7 @@ interface CspFindingCloud { region?: string; } -interface CspFindingResult { +export interface CspFindingResult { evaluation: 'passed' | 'failed'; expected?: Record; evidence: Record; diff --git a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_findings.ts b/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_findings.ts new file mode 100644 index 0000000000000..aee35ca602ef8 --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_findings.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useQuery } from '@tanstack/react-query'; +import { lastValueFrom } from 'rxjs'; +import { CspFinding } from '@kbn/cloud-security-posture-common'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { CoreStart } from '@kbn/core/public'; +import { showErrorToast } from '../..'; +import type { + CspClientPluginStartDeps, + LatestFindingsRequest, + LatestFindingsResponse, + UseMisconfigurationOptions, +} from '../../type'; + +import { useGetCspBenchmarkRulesStatesApi } from './use_get_benchmark_rules_state_api'; +import { + buildMisconfigurationsFindingsQuery, + getMisconfigurationAggregationCount, +} from '../utils/hooks_utils'; + +export const useMisconfigurationFindings = (options: UseMisconfigurationOptions) => { + const { + data, + notifications: { toasts }, + } = useKibana().services; + const { data: rulesStates } = useGetCspBenchmarkRulesStatesApi(); + + return useQuery( + ['csp_misconfiguration_findings', { params: options }, rulesStates], + async () => { + const { + rawResponse: { hits, aggregations }, + } = await lastValueFrom( + data.search.search({ + params: buildMisconfigurationsFindingsQuery(options, rulesStates!), + }) + ); + if (!aggregations) throw new Error('expected aggregations to be defined'); + + return { + count: getMisconfigurationAggregationCount(aggregations.count.buckets), + rows: hits.hits.map((finding) => ({ + result: finding._source?.result, + rule: finding?._source?.rule, + resource: finding?._source?.resource, + })) as Array>, + }; + }, + { + enabled: options.enabled && !!rulesStates, + keepPreviousData: true, + onError: (err: Error) => showErrorToast(toasts, err), + } + ); +}; diff --git a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_preview.ts b/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_preview.ts index af7371da95301..9828bb32c7752 100644 --- a/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_preview.ts +++ b/x-pack/packages/kbn-cloud-security-posture/src/hooks/use_misconfiguration_preview.ts @@ -6,118 +6,22 @@ */ import { useQuery } from '@tanstack/react-query'; import { lastValueFrom } from 'rxjs'; -import type { IKibanaSearchResponse, IKibanaSearchRequest } from '@kbn/search-types'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { - CDR_MISCONFIGURATIONS_INDEX_PATTERN, - LATEST_FINDINGS_RETENTION_POLICY, - CspFinding, -} from '@kbn/cloud-security-posture-common'; -import type { CspBenchmarkRulesStates } from '@kbn/cloud-security-posture-common/schema/rules/latest'; -import { buildMutedRulesFilter } from '@kbn/cloud-security-posture-common'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import type { CoreStart } from '@kbn/core/public'; import { showErrorToast } from '../..'; -import type { CspClientPluginStartDeps } from '../../type'; +import type { + CspClientPluginStartDeps, + LatestFindingsRequest, + LatestFindingsResponse, + UseMisconfigurationOptions, +} from '../../type'; import { useGetCspBenchmarkRulesStatesApi } from './use_get_benchmark_rules_state_api'; +import { + buildMisconfigurationsFindingsQuery, + getMisconfigurationAggregationCount, +} from '../utils/hooks_utils'; -interface MisconfigurationPreviewBaseEsQuery { - query?: { - bool: { - filter: estypes.QueryDslQueryContainer[]; - }; - }; -} - -interface UseMisconfigurationPreviewOptions extends MisconfigurationPreviewBaseEsQuery { - sort: string[][]; - enabled: boolean; - pageSize: number; -} - -type LatestFindingsRequest = IKibanaSearchRequest; -type LatestFindingsResponse = IKibanaSearchResponse< - estypes.SearchResponse ->; - -interface FindingsAggs { - count: estypes.AggregationsMultiBucketAggregateBase; -} - -const RESULT_EVALUATION = { - PASSED: 'passed', - FAILED: 'failed', - UNKNOWN: 'unknown', -}; - -export const getFindingsCountAggQueryMisconfigurationPreview = () => ({ - count: { - filters: { - other_bucket_key: RESULT_EVALUATION.UNKNOWN, - filters: { - [RESULT_EVALUATION.PASSED]: { match: { 'result.evaluation': RESULT_EVALUATION.PASSED } }, - [RESULT_EVALUATION.FAILED]: { match: { 'result.evaluation': RESULT_EVALUATION.FAILED } }, - }, - }, - }, -}); - -export const getMisconfigurationAggregationCount = ( - buckets: estypes.AggregationsBuckets -) => { - return Object.entries(buckets).reduce( - (evaluation, [key, value]) => { - evaluation[key] = (evaluation[key] || 0) + (value.doc_count || 0); - return evaluation; - }, - { - [RESULT_EVALUATION.PASSED]: 0, - [RESULT_EVALUATION.FAILED]: 0, - [RESULT_EVALUATION.UNKNOWN]: 0, - } - ); -}; - -export const buildMisconfigurationsFindingsQuery = ( - { query }: UseMisconfigurationPreviewOptions, - rulesStates: CspBenchmarkRulesStates -) => { - const mutedRulesFilterQuery = buildMutedRulesFilter(rulesStates); - - return { - index: CDR_MISCONFIGURATIONS_INDEX_PATTERN, - size: 0, - aggs: getFindingsCountAggQueryMisconfigurationPreview(), - ignore_unavailable: false, - query: buildMisconfigurationsFindingsQueryWithFilters(query, mutedRulesFilterQuery), - }; -}; - -const buildMisconfigurationsFindingsQueryWithFilters = ( - query: UseMisconfigurationPreviewOptions['query'], - mutedRulesFilterQuery: estypes.QueryDslQueryContainer[] -) => { - return { - ...query, - bool: { - ...query?.bool, - filter: [ - ...(query?.bool?.filter ?? []), - { - range: { - '@timestamp': { - gte: `now-${LATEST_FINDINGS_RETENTION_POLICY}`, - lte: 'now', - }, - }, - }, - ], - must_not: [...mutedRulesFilterQuery], - }, - }; -}; - -export const useMisconfigurationPreview = (options: UseMisconfigurationPreviewOptions) => { +export const useMisconfigurationPreview = (options: UseMisconfigurationOptions) => { const { data, notifications: { toasts }, @@ -134,10 +38,10 @@ export const useMisconfigurationPreview = (options: UseMisconfigurationPreviewOp params: buildMisconfigurationsFindingsQuery(options, rulesStates!), }) ); - if (!aggregations) throw new Error('expected aggregations to be defined'); - + if (!aggregations && !options.ignore_unavailable) + throw new Error('expected aggregations to be defined'); return { - count: getMisconfigurationAggregationCount(aggregations.count.buckets), + count: getMisconfigurationAggregationCount(aggregations?.count?.buckets), }; }, { diff --git a/x-pack/packages/kbn-cloud-security-posture/src/utils/hooks_utils.ts b/x-pack/packages/kbn-cloud-security-posture/src/utils/hooks_utils.ts new file mode 100644 index 0000000000000..a1951f7327b40 --- /dev/null +++ b/x-pack/packages/kbn-cloud-security-posture/src/utils/hooks_utils.ts @@ -0,0 +1,105 @@ +/* + * 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 { + CDR_MISCONFIGURATIONS_INDEX_PATTERN, + LATEST_FINDINGS_RETENTION_POLICY, +} from '@kbn/cloud-security-posture-common'; +import type { CspBenchmarkRulesStates } from '@kbn/cloud-security-posture-common/schema/rules/latest'; +import { buildMutedRulesFilter } from '@kbn/cloud-security-posture-common'; +import type { UseMisconfigurationOptions } from '../../type'; + +const MISCONFIGURATIONS_SOURCE_FIELDS = ['result.*', 'rule.*', 'resource.*']; +interface AggregationBucket { + doc_count?: number; +} + +type AggregationBuckets = Record; + +const RESULT_EVALUATION = { + PASSED: 'passed', + FAILED: 'failed', + UNKNOWN: 'unknown', +}; + +export const getFindingsCountAggQueryMisconfiguration = () => ({ + count: { + filters: { + other_bucket_key: RESULT_EVALUATION.UNKNOWN, + filters: { + [RESULT_EVALUATION.PASSED]: { match: { 'result.evaluation': RESULT_EVALUATION.PASSED } }, + [RESULT_EVALUATION.FAILED]: { match: { 'result.evaluation': RESULT_EVALUATION.FAILED } }, + }, + }, + }, +}); + +export const getMisconfigurationAggregationCount = ( + buckets?: estypes.AggregationsBuckets +) => { + const defaultBuckets: AggregationBuckets = { + [RESULT_EVALUATION.PASSED]: { doc_count: 0 }, + [RESULT_EVALUATION.FAILED]: { doc_count: 0 }, + [RESULT_EVALUATION.UNKNOWN]: { doc_count: 0 }, + }; + + // if buckets are undefined we will use default buckets + const usedBuckets = buckets || defaultBuckets; + return Object.entries(usedBuckets).reduce( + (evaluation, [key, value]) => { + evaluation[key] = (evaluation[key] || 0) + (value.doc_count || 0); + return evaluation; + }, + { + [RESULT_EVALUATION.PASSED]: 0, + [RESULT_EVALUATION.FAILED]: 0, + [RESULT_EVALUATION.UNKNOWN]: 0, + } + ); +}; + +export const buildMisconfigurationsFindingsQuery = ( + { query }: UseMisconfigurationOptions, + rulesStates: CspBenchmarkRulesStates, + isPreview = false +) => { + const mutedRulesFilterQuery = buildMutedRulesFilter(rulesStates); + + return { + index: CDR_MISCONFIGURATIONS_INDEX_PATTERN, + size: isPreview ? 0 : 500, + aggs: getFindingsCountAggQueryMisconfiguration(), + ignore_unavailable: true, + query: buildMisconfigurationsFindingsQueryWithFilters(query, mutedRulesFilterQuery), + _source: MISCONFIGURATIONS_SOURCE_FIELDS, + }; +}; + +const buildMisconfigurationsFindingsQueryWithFilters = ( + query: UseMisconfigurationOptions['query'], + mutedRulesFilterQuery: estypes.QueryDslQueryContainer[] +) => { + return { + ...query, + bool: { + ...query?.bool, + filter: [ + ...(query?.bool?.filter ?? []), + { + range: { + '@timestamp': { + gte: `now-${LATEST_FINDINGS_RETENTION_POLICY}`, + lte: 'now', + }, + }, + }, + ], + must_not: [...mutedRulesFilterQuery], + }, + }; +}; diff --git a/x-pack/packages/kbn-cloud-security-posture/type.ts b/x-pack/packages/kbn-cloud-security-posture/type.ts index 70daabecf67d3..666d432df609d 100644 --- a/x-pack/packages/kbn-cloud-security-posture/type.ts +++ b/x-pack/packages/kbn-cloud-security-posture/type.ts @@ -22,6 +22,9 @@ import type { FleetStart } from '@kbn/fleet-plugin/public'; import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; import { SharePluginStart } from '@kbn/share-plugin/public'; import { SpacesPluginStart } from '@kbn/spaces-plugin/public'; +import { CspFinding } from '@kbn/cloud-security-posture-common'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { IKibanaSearchResponse, IKibanaSearchRequest } from '@kbn/search-types'; import type { BoolQuery } from '@kbn/es-query'; export interface FindingsBaseEsQuery { @@ -51,3 +54,27 @@ export interface CspClientPluginStartDeps { // optional usageCollection?: UsageCollectionStart; } + +export interface MisconfigurationBaseEsQuery { + query?: { + bool: { + filter: estypes.QueryDslQueryContainer[]; + }; + }; +} + +export interface UseMisconfigurationOptions extends MisconfigurationBaseEsQuery { + sort: string[][]; + enabled: boolean; + pageSize: number; + ignore_unavailable?: boolean; +} + +export type LatestFindingsRequest = IKibanaSearchRequest; +export type LatestFindingsResponse = IKibanaSearchResponse< + estypes.SearchResponse +>; + +export interface FindingsAggs { + count: estypes.AggregationsMultiBucketAggregateBase; +} diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/insights_tab_csp.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/insights_tab_csp.tsx new file mode 100644 index 0000000000000..fa91a99c858a8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/insights_tab_csp.tsx @@ -0,0 +1,65 @@ +/* + * 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, { memo } from 'react'; +import { EuiButtonGroup, EuiSpacer } from '@elastic/eui'; +import type { EuiButtonGroupOptionProps } from '@elastic/eui/src/components/button/button_group/button_group'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useExpandableFlyoutState } from '@kbn/expandable-flyout'; +import { MisconfigurationFindingsDetailsTable } from './misconfiguration_findings_details_table'; + +enum InsightsTabCspTab { + MISCONFIGURATION = 'misconfigurationTabId', +} + +const insightsButtons: EuiButtonGroupOptionProps[] = [ + { + id: InsightsTabCspTab.MISCONFIGURATION, + label: ( + + ), + 'data-test-subj': 'misconfigurationTabDataTestId', + }, +]; + +/** + * Insights view displayed in the document details expandable flyout left section + */ +export const InsightsTabCsp = memo( + ({ name, fieldName }: { name: string; fieldName: 'host.name' | 'user.name' }) => { + const panels = useExpandableFlyoutState(); + const activeInsightsId = panels.left?.path?.subTab ?? 'misconfigurationTabId'; + + return ( + <> + {}} + buttonSize="compressed" + isFullWidth + data-test-subj={'insightButtonGroupsTestId'} + /> + + + + ); + } +); + +InsightsTabCsp.displayName = 'InsightsTab'; diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/misconfiguration_findings_details_table.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/misconfiguration_findings_details_table.tsx new file mode 100644 index 0000000000000..1362e0e42e6ba --- /dev/null +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/misconfiguration_findings_details_table.tsx @@ -0,0 +1,184 @@ +/* + * 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, { memo, useState } from 'react'; +import type { Criteria, EuiBasicTableColumn } from '@elastic/eui'; +import { EuiSpacer, EuiIcon, EuiPanel, EuiLink, EuiText, EuiBasicTable } from '@elastic/eui'; +import { useMisconfigurationFindings } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_findings'; +import { i18n } from '@kbn/i18n'; +import type { CspFinding, CspFindingResult } from '@kbn/cloud-security-posture-common'; +import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { DistributionBar } from '@kbn/security-solution-distribution-bar'; +import { useNavigateFindings } from '@kbn/cloud-security-posture/src/hooks/use_navigate_findings'; +import type { CspBenchmarkRuleMetadata } from '@kbn/cloud-security-posture-common/schema/rules/latest'; +import { CspEvaluationBadge } from '@kbn/cloud-security-posture'; + +type MisconfigurationFindingDetailFields = Pick; + +const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: number) => { + if (passedFindingsStats === 0 && failedFindingsStats === 0) return []; + return [ + { + key: i18n.translate( + 'xpack.securitySolution.flyout.right.insights.misconfigurations.passedFindingsText', + { + defaultMessage: 'Passed findings', + } + ), + count: passedFindingsStats, + color: euiThemeVars.euiColorSuccess, + }, + { + key: i18n.translate( + 'xpack.securitySolution.flyout.right.insights.misconfigurations.failedFindingsText', + { + defaultMessage: 'Failed findings', + } + ), + count: failedFindingsStats, + color: euiThemeVars.euiColorVis9, + }, + ]; +}; + +/** + * Insights view displayed in the document details expandable flyout left section + */ +export const MisconfigurationFindingsDetailsTable = memo( + ({ fieldName, queryName }: { fieldName: 'host.name' | 'user.name'; queryName: string }) => { + const { data } = useMisconfigurationFindings({ + query: buildEntityFlyoutPreviewQuery(fieldName, queryName), + sort: [], + enabled: true, + pageSize: 1, + }); + + const passedFindings = data?.count.passed || 0; + const failedFindings = data?.count.failed || 0; + + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(10); + + const findingsPagination = (findings: MisconfigurationFindingDetailFields[]) => { + let pageOfItems; + + if (!pageIndex && !pageSize) { + pageOfItems = findings; + } else { + const startIndex = pageIndex * pageSize; + pageOfItems = findings?.slice( + startIndex, + Math.min(startIndex + pageSize, findings?.length) + ); + } + + return { + pageOfItems, + totalItemCount: findings?.length, + }; + }; + + const { pageOfItems, totalItemCount } = findingsPagination(data?.rows || []); + + const pagination = { + pageIndex, + pageSize, + totalItemCount, + pageSizeOptions: [10, 25, 100], + }; + + const onTableChange = ({ page }: Criteria) => { + if (page) { + const { index, size } = page; + setPageIndex(index); + setPageSize(size); + } + }; + + const navToFindings = useNavigateFindings(); + + const navToFindingsByHostName = (hostName: string) => { + navToFindings({ 'host.name': hostName }, ['rule.name']); + }; + + const navToFindingsByRuleAndResourceId = (ruleId: string, resourceId: string) => { + navToFindings({ 'rule.id': ruleId, 'resource.id': resourceId }); + }; + + const columns: Array> = [ + { + field: 'rule', + name: '', + width: '5%', + render: (rule: CspBenchmarkRuleMetadata, finding: MisconfigurationFindingDetailFields) => ( + { + navToFindingsByRuleAndResourceId(rule?.id, finding?.resource?.id); + }} + > + + + ), + }, + { + field: 'result', + render: (result: CspFindingResult) => , + name: i18n.translate( + 'xpack.securitySolution.flyout.left.insights.misconfigurations.table.resultColumnName', + { + defaultMessage: 'Result', + } + ), + width: '10%', + }, + { + field: 'rule', + render: (rule: CspBenchmarkRuleMetadata) => {rule?.name}, + name: i18n.translate( + 'xpack.securitySolution.flyout.left.insights.misconfigurations.table.ruleColumnName', + { + defaultMessage: 'Rule', + } + ), + width: '90%', + }, + ]; + + return ( + <> + + { + navToFindingsByHostName(queryName); + }} + > + {i18n.translate( + 'xpack.securitySolution.flyout.left.insights.misconfigurations.tableTitle', + { + defaultMessage: 'Misconfigurations', + } + )} + + + + + + + + + ); + } +); + +MisconfigurationFindingsDetailsTable.displayName = 'MisconfigurationFindingsDetailsTable'; diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx index 3ae2acadcf8e4..f6ba0389f752a 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { css } from '@emotion/react'; import type { EuiThemeComputed } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, useEuiTheme, EuiTitle } from '@elastic/eui'; @@ -16,6 +16,16 @@ import { euiThemeVars } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; import { ExpandablePanel } from '@kbn/security-solution-common'; import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { HostDetailsPanelKey } from '../../../flyout/entity_details/host_details_left'; +import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score'; +import { RiskScoreEntity } from '../../../../common/entity_analytics/risk_engine'; +import { buildHostNamesFilter } from '../../../../common/search_strategy'; + +const FIRST_RECORD_PAGINATION = { + cursorStart: 0, + querySize: 1, +}; const getFindingsStats = (passedFindingsStats: number, failedFindingsStats: number) => { if (passedFindingsStats === 0 && failedFindingsStats === 0) return []; @@ -75,10 +85,14 @@ const MisconfigurationPreviewScore = ({ passedFindings, failedFindings, euiTheme, + numberOfPassedFindings, + numberOfFailedFindings, }: { passedFindings: number; failedFindings: number; euiTheme: EuiThemeComputed<{}>; + numberOfPassedFindings?: number; + numberOfFailedFindings?: number; }) => { return ( @@ -119,9 +133,52 @@ export const MisconfigurationsPreview = ({ hostName }: { hostName: string }) => const { euiTheme } = useEuiTheme(); const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0; + const hostNameFilterQuery = useMemo( + () => (hostName ? buildHostNamesFilter([hostName]) : undefined), + [hostName] + ); + + const riskScoreState = useRiskScore({ + riskEntity: RiskScoreEntity.host, + filterQuery: hostNameFilterQuery, + onlyLatest: false, + pagination: FIRST_RECORD_PAGINATION, + }); + const { data: hostRisk } = riskScoreState; + const hostRiskData = hostRisk && hostRisk.length > 0 ? hostRisk[0] : undefined; + const isRiskScoreExist = !!hostRiskData?.host.risk; + const { openLeftPanel } = useExpandableFlyoutApi(); + const isPreviewMode = false; + const goToEntityInsightTab = useCallback(() => { + openLeftPanel({ + id: HostDetailsPanelKey, + params: { + name: hostName, + isRiskScoreExist, + hasMisconfigurationFindings, + path: { tab: 'csp_insights' }, + }, + }); + }, [hasMisconfigurationFindings, hostName, isRiskScoreExist, openLeftPanel]); + const link = useMemo( + () => + !isPreviewMode + ? { + callback: goToEntityInsightTab, + tooltip: ( + + ), + } + : undefined, + [isPreviewMode, goToEntityInsightTab] + ); return ( /> ), + link: hasMisconfigurationFindings ? link : undefined, }} data-test-subj={'securitySolutionFlyoutInsightsMisconfigurations'} > diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/index.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/index.tsx index c863904da6c66..2a94ee0438293 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/index.tsx @@ -11,8 +11,10 @@ import { EntityDetailsLeftPanelTab } from '../../../flyout/entity_details/shared import { PREFIX } from '../../../flyout/shared/test_ids'; import type { RiskInputsTabProps } from './tabs/risk_inputs/risk_inputs_tab'; import { RiskInputsTab } from './tabs/risk_inputs/risk_inputs_tab'; +import { InsightsTabCsp } from '../../../cloud_security_posture/components/csp_details/insights_tab_csp'; export const RISK_INPUTS_TAB_TEST_ID = `${PREFIX}RiskInputsTab` as const; +export const INSIGHTS_TAB_TEST_ID = `${PREFIX}InsightInputsTab` as const; export const getRiskInputTab = ({ entityType, entityName, scopeId }: RiskInputsTabProps) => ({ id: EntityDetailsLeftPanelTab.RISK_INPUTS, @@ -25,3 +27,21 @@ export const getRiskInputTab = ({ entityType, entityName, scopeId }: RiskInputsT ), content: , }); + +export const getInsightsInputTab = ({ + name, + fieldName, +}: { + name: string; + fieldName: 'host.name' | 'user.name'; +}) => ({ + id: EntityDetailsLeftPanelTab.CSP_INSIGHTS, + 'data-test-subj': INSIGHTS_TAB_TEST_ID, + name: ( + + ), + content: , +}); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.test.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.test.tsx index afe0bebcf802b..c07159c3db6cd 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.test.tsx @@ -5,7 +5,10 @@ * 2.0. */ -import { RISK_INPUTS_TAB_TEST_ID } from '../../../entity_analytics/components/entity_details_flyout'; +import { + RISK_INPUTS_TAB_TEST_ID, + INSIGHTS_TAB_TEST_ID, +} from '../../../entity_analytics/components/entity_details_flyout'; import { render } from '@testing-library/react'; import React from 'react'; import { HostDetailsPanel } from '.'; @@ -59,4 +62,34 @@ describe('HostDetailsPanel', () => { ); expect(queryByTestId(RISK_INPUTS_TAB_TEST_ID)).not.toBeInTheDocument(); }); + + it("doesn't render insights panel when there no misconfiguration findings", () => { + const { queryByTestId } = render( + , + { + wrapper: TestProviders, + } + ); + expect(queryByTestId(INSIGHTS_TAB_TEST_ID)).not.toBeInTheDocument(); + }); + + it('render insights panel when there are misconfiguration findings', () => { + const { queryByTestId } = render( + , + { + wrapper: TestProviders, + } + ); + expect(queryByTestId(INSIGHTS_TAB_TEST_ID)).toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx index ba34ac3d8aa3a..4d7ffc641ffc7 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.tsx @@ -5,9 +5,12 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; -import { getRiskInputTab } from '../../../entity_analytics/components/entity_details_flyout'; +import { + getRiskInputTab, + getInsightsInputTab, +} from '../../../entity_analytics/components/entity_details_flyout'; import { LeftPanelContent } from '../shared/components/left_panel/left_panel_content'; import { EntityDetailsLeftPanelTab, @@ -19,6 +22,10 @@ export interface HostDetailsPanelProps extends Record { isRiskScoreExist: boolean; name: string; scopeId: string; + hasMisconfigurationFindings?: boolean; + path?: { + tab?: EntityDetailsLeftPanelTab; + }; } export interface HostDetailsExpandableFlyoutProps extends FlyoutPanelProps { key: 'host_details'; @@ -26,18 +33,31 @@ export interface HostDetailsExpandableFlyoutProps extends FlyoutPanelProps { } export const HostDetailsPanelKey: HostDetailsExpandableFlyoutProps['key'] = 'host_details'; -export const HostDetailsPanel = ({ name, isRiskScoreExist, scopeId }: HostDetailsPanelProps) => { - // Temporary implementation while Host details left panel don't have Asset tabs - const [tabs, selectedTabId, setSelectedTabId] = useMemo(() => { +export const HostDetailsPanel = ({ + name, + isRiskScoreExist, + scopeId, + path, + hasMisconfigurationFindings, +}: HostDetailsPanelProps) => { + const [selectedTabId, setSelectedTabId] = useState( + path?.tab === EntityDetailsLeftPanelTab.CSP_INSIGHTS + ? EntityDetailsLeftPanelTab.CSP_INSIGHTS + : EntityDetailsLeftPanelTab.RISK_INPUTS + ); + + const [tabs] = useMemo(() => { const isRiskScoreTabAvailable = isRiskScoreExist && name; - return [ - isRiskScoreTabAvailable - ? [getRiskInputTab({ entityName: name, entityType: RiskScoreEntity.host, scopeId })] - : [], - EntityDetailsLeftPanelTab.RISK_INPUTS, - () => {}, - ]; - }, [name, isRiskScoreExist, scopeId]); + const riskScoreTab = isRiskScoreTabAvailable + ? [getRiskInputTab({ entityName: name, entityType: RiskScoreEntity.host, scopeId })] + : []; + + // Determine if the Insights tab should be included + const insightsTab = hasMisconfigurationFindings + ? [getInsightsInputTab({ name, fieldName: 'host.name' })] + : []; + return [[...riskScoreTab, ...insightsTab], EntityDetailsLeftPanelTab.RISK_INPUTS, () => {}]; + }, [isRiskScoreExist, name, scopeId, hasMisconfigurationFindings]); return ( <> diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx index 4799c396a7be3..42280e60ef46e 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx @@ -10,6 +10,8 @@ import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { FlyoutLoading, FlyoutNavigation } from '@kbn/security-solution-common'; +import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common'; +import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; import { useRefetchQueryById } from '../../../entity_analytics/api/hooks/use_refetch_query_by_id'; import { RISK_INPUTS_TAB_QUERY_ID } from '../../../entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs_tab'; import type { Refetch } from '../../../common/types'; @@ -28,7 +30,7 @@ import { AnomalyTableProvider } from '../../../common/components/ml/anomaly/anom import type { ObservedEntityData } from '../shared/components/observed_entity/types'; import { useObservedHost } from './hooks/use_observed_host'; import { HostDetailsPanelKey } from '../host_details_left'; -import type { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; +import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; import { HostPreviewPanelFooter } from '../host_preview/footer'; export interface HostPanelProps extends Record { @@ -92,6 +94,19 @@ export const HostPanel = ({ { onSuccess: refetchRiskScore } ); + const { data } = useMisconfigurationPreview({ + query: buildEntityFlyoutPreviewQuery('host.name', hostName), + sort: [], + enabled: true, + pageSize: 1, + ignore_unavailable: true, + }); + + const passedFindings = data?.count.passed || 0; + const failedFindings = data?.count.failed || 0; + + const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0; + useQueryInspector({ deleteQuery, inspect: inspectRiskScore, @@ -114,13 +129,23 @@ export const HostPanel = ({ scopeId, isRiskScoreExist, path: tab ? { tab } : undefined, + hasMisconfigurationFindings, }, }); }, - [telemetry, openLeftPanel, hostName, isRiskScoreExist, scopeId] + [telemetry, openLeftPanel, hostName, scopeId, isRiskScoreExist, hasMisconfigurationFindings] + ); + + const openDefaultPanel = useCallback( + () => + openTabPanel( + isRiskScoreExist + ? EntityDetailsLeftPanelTab.RISK_INPUTS + : EntityDetailsLeftPanelTab.CSP_INSIGHTS + ), + [isRiskScoreExist, openTabPanel] ); - const openDefaultPanel = useCallback(() => openTabPanel(), [openTabPanel]); const observedHost = useObservedHost(hostName, scopeId); if (observedHost.isLoading) { @@ -147,7 +172,9 @@ export const HostPanel = ({ return ( <> diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx index 7a537d64aa755..a33911c928aaf 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/shared/components/left_panel/left_panel_header.tsx @@ -22,6 +22,7 @@ export enum EntityDetailsLeftPanelTab { RISK_INPUTS = 'risk_inputs', OKTA = 'okta_document', ENTRA = 'entra_document', + CSP_INSIGHTS = 'csp_insights', } export interface PanelHeaderProps { @@ -65,9 +66,7 @@ export const LeftPanelHeader: VFC = memo( border-block-end: none !important; `} > - - {renderTabs} - + {renderTabs} ); } From 676d73fbe502008d088af954aae58556f2baab0e Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 19 Sep 2024 19:57:32 +1000 Subject: [PATCH 04/19] [8.x] [ResponseOps][MW] Add telemetry for the maintenance window (#192483) (#193395) # Backport This will backport the following commits from `main` to `8.x`: - [[ResponseOps][MW] Add telemetry for the maintenance window (#192483)](https://github.com/elastic/kibana/pull/192483) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Julia --- .../server/usage/alerting_usage_collector.ts | 6 + .../lib/get_telemetry_from_kibana.test.ts | 178 +++++++++++++++++- .../usage/lib/get_telemetry_from_kibana.ts | 77 +++++++- x-pack/plugins/alerting/server/usage/task.ts | 39 +++- .../alerting/server/usage/task_state.ts | 6 + x-pack/plugins/alerting/server/usage/types.ts | 3 + .../schema/xpack_plugins.json | 9 + .../alerting_and_actions_telemetry.ts | 59 +++++- 8 files changed, 364 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/alerting/server/usage/alerting_usage_collector.ts b/x-pack/plugins/alerting/server/usage/alerting_usage_collector.ts index 5efd2d7a49152..46cf58d838ecc 100644 --- a/x-pack/plugins/alerting/server/usage/alerting_usage_collector.ts +++ b/x-pack/plugins/alerting/server/usage/alerting_usage_collector.ts @@ -205,6 +205,9 @@ export function createAlertingUsageCollector( count_rules_with_tags: 0, count_rules_snoozed: 0, count_rules_muted: 0, + count_mw_total: 0, + count_mw_with_repeat_toggle_on: 0, + count_mw_with_filter_alert_toggle_on: 0, count_rules_with_muted_alerts: 0, count_connector_types_by_consumers: {}, count_rules_by_execution_status_per_day: {}, @@ -289,6 +292,9 @@ export function createAlertingUsageCollector( count_rules_by_notify_when: byNotifyWhenSchema, count_rules_snoozed: { type: 'long' }, count_rules_muted: { type: 'long' }, + count_mw_total: { type: 'long' }, + count_mw_with_repeat_toggle_on: { type: 'long' }, + count_mw_with_filter_alert_toggle_on: { type: 'long' }, count_rules_with_muted_alerts: { type: 'long' }, count_connector_types_by_consumers: { DYNAMIC_KEY: { DYNAMIC_KEY: { type: 'long' } } }, count_rules_by_execution_status_per_day: byStatusPerDaySchema, diff --git a/x-pack/plugins/alerting/server/usage/lib/get_telemetry_from_kibana.test.ts b/x-pack/plugins/alerting/server/usage/lib/get_telemetry_from_kibana.test.ts index f29602458fd50..7c06e9867dae3 100644 --- a/x-pack/plugins/alerting/server/usage/lib/get_telemetry_from_kibana.test.ts +++ b/x-pack/plugins/alerting/server/usage/lib/get_telemetry_from_kibana.test.ts @@ -6,11 +6,97 @@ */ import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; -import { getTotalCountAggregations, getTotalCountInUse } from './get_telemetry_from_kibana'; +import { + getTotalCountAggregations, + getTotalCountInUse, + getMWTelemetry, +} from './get_telemetry_from_kibana'; +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; +import { MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE } from '../../../common'; +import { ISavedObjectsRepository } from '@kbn/core/server'; const elasticsearch = elasticsearchServiceMock.createStart(); const esClient = elasticsearch.client.asInternalUser; const logger: ReturnType = loggingSystemMock.createLogger(); +const savedObjectsClient = savedObjectsClientMock.create() as unknown as ISavedObjectsRepository; +const thrownError = new Error('Fail'); + +const mockedResponse = { + saved_objects: [ + { + id: '1', + type: MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE, + attributes: { + title: 'test_rule_1', + enabled: true, + duration: 1800000, + expirationDate: '2025-09-09T13:13:07.824Z', + events: [], + rRule: { + dtstart: '2024-09-09T13:13:02.054Z', + tzid: 'Europe/Stockholm', + freq: 0, + count: 1, + }, + createdBy: null, + updatedBy: null, + createdAt: '2024-09-09T13:13:07.825Z', + updatedAt: '2024-09-09T13:13:07.825Z', + scopedQuery: null, + }, + }, + { + id: '2', + type: MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE, + attributes: { + title: 'test_rule_2', + enabled: true, + duration: 1800000, + expirationDate: '2025-09-09T13:13:07.824Z', + events: [], + rRule: { + dtstart: '2024-09-09T13:13:02.054Z', + tzid: 'Europe/Stockholm', + freq: 3, + interval: 1, + byweekday: ['SU'], + }, + createdBy: null, + updatedBy: null, + createdAt: '2024-09-09T13:13:07.825Z', + updatedAt: '2024-09-09T13:13:07.825Z', + scopedQuery: { + filters: [], + kql: 'kibana.alert.job_errors_results.job_id : * ', + dsl: '{"bool":{"must":[],"filter":[{"bool":{"should":[{"exists":{"field":"kibana.alert.job_errors_results.job_id"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}', + }, + }, + }, + { + id: '3', + type: MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE, + attributes: { + title: 'test_rule_3', + enabled: true, + duration: 1800000, + expirationDate: '2025-09-09T13:13:07.824Z', + events: [], + rRule: { + dtstart: '2024-09-09T13:13:02.054Z', + tzid: 'Europe/Stockholm', + freq: 3, + interval: 1, + byweekday: ['TU'], + }, + createdBy: null, + updatedBy: null, + createdAt: '2024-09-09T13:13:07.825Z', + updatedAt: '2024-09-09T13:13:07.825Z', + scopedQuery: null, + }, + }, + ], +}; describe('kibana index telemetry', () => { beforeEach(() => { @@ -420,4 +506,94 @@ describe('kibana index telemetry', () => { }); }); }); + + describe('getMWTelemetry', () => { + test('should return MW telemetry', async () => { + savedObjectsClient.createPointInTimeFinder = jest.fn().mockReturnValue({ + close: jest.fn(), + find: jest.fn().mockImplementation(async function* () { + yield mockedResponse; + }), + }); + const telemetry = await getMWTelemetry({ + savedObjectsClient, + logger, + }); + + expect(savedObjectsClient.createPointInTimeFinder).toHaveBeenCalledWith({ + type: MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE, + namespaces: ['*'], + perPage: 100, + fields: ['rRule', 'scopedQuery'], + }); + expect(telemetry).toStrictEqual({ + count_mw_total: 3, + count_mw_with_repeat_toggle_on: 2, + count_mw_with_filter_alert_toggle_on: 1, + hasErrors: false, + }); + }); + }); + + test('should throw the error', async () => { + savedObjectsClient.createPointInTimeFinder = jest.fn().mockReturnValue({ + close: jest.fn(), + find: jest.fn().mockImplementation(async function* () { + throw thrownError; + }), + }); + + const telemetry = await getMWTelemetry({ + savedObjectsClient, + logger, + }); + + expect(savedObjectsClient.createPointInTimeFinder).toHaveBeenCalledWith({ + type: MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE, + namespaces: ['*'], + perPage: 100, + fields: ['rRule', 'scopedQuery'], + }); + + expect(telemetry).toStrictEqual({ + count_mw_total: 0, + count_mw_with_repeat_toggle_on: 0, + count_mw_with_filter_alert_toggle_on: 0, + hasErrors: true, + errorMessage: 'Fail', + }); + expect(logger.warn).toHaveBeenCalled(); + const loggerCall = logger.warn.mock.calls[0][0]; + const loggerMeta = logger.warn.mock.calls[0][1]; + expect(loggerCall).toBe('Error executing alerting telemetry task: getTotalMWCount - {}'); + expect(loggerMeta?.tags).toEqual(['alerting', 'telemetry-failed']); + expect(loggerMeta?.error?.stack_trace).toBeDefined(); + }); + + test('should stop on MW max limit count', async () => { + savedObjectsClient.createPointInTimeFinder = jest.fn().mockReturnValue({ + close: jest.fn(), + find: jest.fn().mockImplementation(async function* () { + yield mockedResponse; + }), + }); + const telemetry = await getMWTelemetry({ + savedObjectsClient, + logger, + maxDocuments: 1, + }); + + expect(savedObjectsClient.createPointInTimeFinder).toHaveBeenCalledWith({ + type: MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE, + namespaces: ['*'], + perPage: 100, + fields: ['rRule', 'scopedQuery'], + }); + expect(telemetry).toStrictEqual({ + count_mw_total: 2, + count_mw_with_repeat_toggle_on: 1, + count_mw_with_filter_alert_toggle_on: 1, + hasErrors: false, + }); + }); }); diff --git a/x-pack/plugins/alerting/server/usage/lib/get_telemetry_from_kibana.ts b/x-pack/plugins/alerting/server/usage/lib/get_telemetry_from_kibana.ts index fdfdbf1dbcfe6..756512815d901 100644 --- a/x-pack/plugins/alerting/server/usage/lib/get_telemetry_from_kibana.ts +++ b/x-pack/plugins/alerting/server/usage/lib/get_telemetry_from_kibana.ts @@ -11,7 +11,7 @@ import type { AggregationsTermsAggregateBase, AggregationsStringTermsBucketKeys, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { ElasticsearchClient, Logger } from '@kbn/core/server'; +import { ElasticsearchClient, Logger, ISavedObjectsRepository } from '@kbn/core/server'; import { ConnectorsByConsumersBucket, @@ -23,6 +23,8 @@ import { AlertingUsage } from '../types'; import { NUM_ALERTING_RULE_TYPES } from '../alerting_usage_collector'; import { parseSimpleRuleTypeBucket } from './parse_simple_rule_type_bucket'; import { groupRulesBySearchType } from './group_rules_by_search_type'; +import { MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE } from '../../../common'; +import { MaintenanceWindowAttributes } from '../../data/maintenance_window/types'; interface Opts { esClient: ElasticsearchClient; @@ -30,6 +32,12 @@ interface Opts { logger: Logger; } +interface MWOpts { + savedObjectsClient: ISavedObjectsRepository; + logger: Logger; + maxDocuments?: number; +} + type GetTotalCountsResults = Pick< AlertingUsage, | 'count_total' @@ -48,6 +56,14 @@ type GetTotalCountsResults = Pick< | 'connectors_per_alert' > & { errorMessage?: string; hasErrors: boolean }; +type GetMWTelemetryResults = Pick< + AlertingUsage, + 'count_mw_total' | 'count_mw_with_repeat_toggle_on' | 'count_mw_with_filter_alert_toggle_on' +> & { + errorMessage?: string; + hasErrors: boolean; +}; + interface GetTotalCountInUseResults { countTotal: number; countByType: Record; @@ -56,6 +72,8 @@ interface GetTotalCountInUseResults { hasErrors: boolean; } +const TELEMETRY_MW_COUNT_LIMIT = 10000; + export async function getTotalCountAggregations({ esClient, alertIndex, @@ -490,3 +508,60 @@ export async function getTotalCountInUse({ }; } } + +export async function getMWTelemetry({ + savedObjectsClient, + logger, + maxDocuments = TELEMETRY_MW_COUNT_LIMIT, +}: MWOpts): Promise { + try { + const mwFinder = savedObjectsClient.createPointInTimeFinder({ + type: MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE, + namespaces: ['*'], + perPage: 100, + fields: ['rRule', 'scopedQuery'], + }); + + let countMWTotal = 0; + let countMWWithRepeatToggleON = 0; + let countMWWithFilterAlertToggleON = 0; + mwLoop: for await (const response of mwFinder.find()) { + for (const mwSavedObject of response.saved_objects) { + if (countMWTotal > maxDocuments) break mwLoop; + countMWTotal = countMWTotal + 1; + // scopedQuery property will be null if "Filter alerts" toggle will be off + if (mwSavedObject.attributes.scopedQuery) { + countMWWithFilterAlertToggleON = countMWWithFilterAlertToggleON + 1; + } + // interval property will be not in place if "Repeat" toggle will be off + if (Object.hasOwn(mwSavedObject.attributes.rRule, 'interval')) { + countMWWithRepeatToggleON = countMWWithRepeatToggleON + 1; + } + } + } + await mwFinder.close(); + + return { + hasErrors: false, + count_mw_total: countMWTotal, + count_mw_with_repeat_toggle_on: countMWWithRepeatToggleON, + count_mw_with_filter_alert_toggle_on: countMWWithFilterAlertToggleON, + }; + } catch (err) { + const errorMessage = err?.message ? err.message : err.toString(); + logger.warn( + `Error executing alerting telemetry task: getTotalMWCount - ${JSON.stringify(err)}`, + { + tags: ['alerting', 'telemetry-failed'], + error: { stack_trace: err?.stack }, + } + ); + return { + hasErrors: true, + errorMessage, + count_mw_total: 0, + count_mw_with_repeat_toggle_on: 0, + count_mw_with_filter_alert_toggle_on: 0, + }; + } +} diff --git a/x-pack/plugins/alerting/server/usage/task.ts b/x-pack/plugins/alerting/server/usage/task.ts index 0cc08db911226..41db75032ef4d 100644 --- a/x-pack/plugins/alerting/server/usage/task.ts +++ b/x-pack/plugins/alerting/server/usage/task.ts @@ -12,15 +12,19 @@ import { TaskManagerStartContract, IntervalSchedule, } from '@kbn/task-manager-plugin/server'; - import { getFailedAndUnrecognizedTasksPerDay } from './lib/get_telemetry_from_task_manager'; -import { getTotalCountAggregations, getTotalCountInUse } from './lib/get_telemetry_from_kibana'; +import { + getTotalCountAggregations, + getTotalCountInUse, + getMWTelemetry, +} from './lib/get_telemetry_from_kibana'; import { getExecutionsPerDayCount, getExecutionTimeoutsPerDayCount, } from './lib/get_telemetry_from_event_log'; import { stateSchemaByVersion, emptyState, type LatestTaskStateSchema } from './task_state'; import { RULE_SAVED_OBJECT_TYPE } from '../saved_objects'; +import { MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE } from '../../common'; export const TELEMETRY_TASK_TYPE = 'alerting_telemetry'; @@ -36,12 +40,6 @@ export function initializeAlertingTelemetry( registerAlertingTelemetryTask(logger, core, taskManager, eventLogIndex); } -export function scheduleAlertingTelemetry(logger: Logger, taskManager?: TaskManagerStartContract) { - if (taskManager) { - scheduleTasks(logger, taskManager).catch(() => {}); // it shouldn't reject, but just in case - } -} - function registerAlertingTelemetryTask( logger: Logger, core: CoreSetup, @@ -58,6 +56,12 @@ function registerAlertingTelemetryTask( }); } +export function scheduleAlertingTelemetry(logger: Logger, taskManager?: TaskManagerStartContract) { + if (taskManager) { + scheduleTasks(logger, taskManager).catch(() => {}); // it shouldn't reject, but just in case + } +} + async function scheduleTasks(logger: Logger, taskManager: TaskManagerStartContract) { try { await taskManager.ensureScheduled({ @@ -93,16 +97,26 @@ export function telemetryTaskRunner( .getStartServices() .then(([coreStart]) => coreStart.savedObjects.getIndexForType(RULE_SAVED_OBJECT_TYPE)); + const getSavedObjectClient = () => + core + .getStartServices() + .then(([coreStart]) => + coreStart.savedObjects.createInternalRepository([MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE]) + ); + return { async run() { const esClient = await getEsClient(); const alertIndex = await getAlertIndex(); + const savedObjectsClient = await getSavedObjectClient(); + return Promise.all([ getTotalCountAggregations({ esClient, alertIndex, logger }), getTotalCountInUse({ esClient, alertIndex, logger }), getExecutionsPerDayCount({ esClient, eventLogIndex, logger }), getExecutionTimeoutsPerDayCount({ esClient, eventLogIndex, logger }), getFailedAndUnrecognizedTasksPerDay({ esClient, taskManagerIndex, logger }), + getMWTelemetry({ logger, savedObjectsClient }), ]) .then( ([ @@ -111,13 +125,15 @@ export function telemetryTaskRunner( dailyExecutionCounts, dailyExecutionTimeoutCounts, dailyFailedAndUnrecognizedTasks, + MWTelemetry, ]) => { const hasErrors = totalCountAggregations.hasErrors || totalInUse.hasErrors || dailyExecutionCounts.hasErrors || dailyExecutionTimeoutCounts.hasErrors || - dailyFailedAndUnrecognizedTasks.hasErrors; + dailyFailedAndUnrecognizedTasks.hasErrors || + MWTelemetry.hasErrors; const errorMessages = [ totalCountAggregations.errorMessage, @@ -125,6 +141,7 @@ export function telemetryTaskRunner( dailyExecutionCounts.errorMessage, dailyExecutionTimeoutCounts.errorMessage, dailyFailedAndUnrecognizedTasks.errorMessage, + MWTelemetry.errorMessage, ].filter((message) => message !== undefined); const updatedState: LatestTaskStateSchema = { @@ -147,6 +164,10 @@ export function telemetryTaskRunner( count_rules_by_notify_when: totalCountAggregations.count_rules_by_notify_when, count_rules_snoozed: totalCountAggregations.count_rules_snoozed, count_rules_muted: totalCountAggregations.count_rules_muted, + count_mw_total: MWTelemetry.count_mw_total, + count_mw_with_repeat_toggle_on: MWTelemetry.count_mw_with_repeat_toggle_on, + count_mw_with_filter_alert_toggle_on: + MWTelemetry.count_mw_with_filter_alert_toggle_on, count_rules_with_muted_alerts: totalCountAggregations.count_rules_with_muted_alerts, count_connector_types_by_consumers: totalCountAggregations.count_connector_types_by_consumers, diff --git a/x-pack/plugins/alerting/server/usage/task_state.ts b/x-pack/plugins/alerting/server/usage/task_state.ts index cbcabeb490b84..a9652ee8200a1 100644 --- a/x-pack/plugins/alerting/server/usage/task_state.ts +++ b/x-pack/plugins/alerting/server/usage/task_state.ts @@ -146,6 +146,9 @@ export const stateSchemaByVersion = { }), count_rules_snoozed: schema.number(), count_rules_muted: schema.number(), + count_mw_total: schema.number(), + count_mw_with_repeat_toggle_on: schema.number(), + count_mw_with_filter_alert_toggle_on: schema.number(), count_rules_with_muted_alerts: schema.number(), count_connector_types_by_consumers: schema.recordOf( schema.string(), @@ -248,6 +251,9 @@ export const emptyState: LatestTaskStateSchema = { }, count_rules_snoozed: 0, count_rules_muted: 0, + count_mw_total: 0, + count_mw_with_repeat_toggle_on: 0, + count_mw_with_filter_alert_toggle_on: 0, count_rules_with_muted_alerts: 0, count_connector_types_by_consumers: {}, count_rules_namespaces: 0, diff --git a/x-pack/plugins/alerting/server/usage/types.ts b/x-pack/plugins/alerting/server/usage/types.ts index 15c0f0a962710..ece69ace7ba5d 100644 --- a/x-pack/plugins/alerting/server/usage/types.ts +++ b/x-pack/plugins/alerting/server/usage/types.ts @@ -41,6 +41,9 @@ export interface AlertingUsage { count_connector_types_by_consumers: Record>; count_rules_snoozed: number; count_rules_muted: number; + count_mw_total: number; + count_mw_with_repeat_toggle_on: number; + count_mw_with_filter_alert_toggle_on: number; count_rules_with_muted_alerts: number; count_rules_by_execution_status_per_day: Record; percentile_num_generated_actions_per_day: { diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 11c1a0a7edee0..ff79fcf4632a3 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -1724,6 +1724,15 @@ "count_rules_muted": { "type": "long" }, + "count_mw_total": { + "type": "long" + }, + "count_mw_with_repeat_toggle_on": { + "type": "long" + }, + "count_mw_with_filter_alert_toggle_on": { + "type": "long" + }, "count_rules_with_muted_alerts": { "type": "long" }, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts index 000ff81d2d2dc..447a49bba4938 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts @@ -90,6 +90,44 @@ export default function createAlertingAndActionsTelemetryTests({ getService }: F return ruleResponse.body.id; } + async function createMaintenanceWindow({ + spaceId, + interval, + scopedQuery = null, + }: { + spaceId: string; + interval?: number; + scopedQuery?: { + filters: string[]; + kql: string; + dsl: string; + } | null; + }) { + const response = await supertestWithoutAuth + .post(`${getUrlPrefix(spaceId)}/internal/alerting/rules/maintenance_window`) + .set('kbn-xsrf', 'foo') + .auth(Superuser.username, Superuser.password) + .send({ + title: 'test-maintenance-window', + duration: 60 * 60 * 1000, // 1 hr + r_rule: { + dtstart: new Date().toISOString(), + tzid: 'UTC', + freq: 0, + count: 1, + ...(interval ? { interval } : {}), + }, + category_ids: ['management'], + scoped_query: scopedQuery, + }); + + expect(response.status).to.equal(200); + + objectRemover.add(spaceId, response.body.id, 'rules/maintenance_window', 'alerting', true); + + return response.body.id; + } + async function setup() { // Create rules and connectors in multiple spaces for (const space of Spaces) { @@ -216,6 +254,18 @@ export default function createAlertingAndActionsTelemetryTests({ getService }: F actions: [], }, }); + // MW with both toggles off + await createMaintenanceWindow({ spaceId: space.id }); + // MW with 'Repeat' toggle on and 'Filter alerts' toggle on + await createMaintenanceWindow({ + spaceId: space.id, + interval: 1, + scopedQuery: { + filters: [], + kql: 'kibana.alert.job_errors_results.job_id : * ', + dsl: '{"bool":{"must":[],"filter":[{"bool":{"should":[{"exists":{"field":"kibana.alert.job_errors_results.job_id"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}', + }, + }); } } @@ -500,6 +550,11 @@ export default function createAlertingAndActionsTelemetryTests({ getService }: F expect(telemetry.count_rules_by_execution_status_per_day.failure > 0).to.be(true); expect(telemetry.count_rules_by_execution_status_per_day.success > 0).to.be(true); + + // maintenance window telemetry + expect(telemetry.count_mw_total).to.equal(6); + expect(telemetry.count_mw_with_filter_alert_toggle_on).to.equal(3); + expect(telemetry.count_mw_with_repeat_toggle_on).to.equal(3); } it('should retrieve telemetry data in the expected format', async () => { @@ -527,7 +582,7 @@ export default function createAlertingAndActionsTelemetryTests({ getService }: F let actionsTelemetry: any; await retry.try(async () => { const telemetryTask = await es.get({ - id: `task:Actions-actions_telemetry`, + id: 'task:Actions-actions_telemetry', index: '.kibana_task_manager', }); expect(telemetryTask!._source!.task?.status).to.be('idle'); @@ -550,7 +605,7 @@ export default function createAlertingAndActionsTelemetryTests({ getService }: F let alertingTelemetry: any; await retry.try(async () => { const telemetryTask = await es.get({ - id: `task:Alerting-alerting_telemetry`, + id: 'task:Alerting-alerting_telemetry', index: '.kibana_task_manager', }); expect(telemetryTask!._source!.task?.status).to.be('idle'); From ef3757657a557eec063e854462942c9cd584b655 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 19 Sep 2024 22:38:21 +1000 Subject: [PATCH 05/19] [8.x] [Security Solution] Adds enable on install UI workflow to prebuilt rules page (#191529) (#193368) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [[Security Solution] Adds enable on install UI workflow to prebuilt rules page (#191529)](https://github.com/elastic/kibana/pull/191529) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Co-authored-by: Elastic Machine --- ...perform_specific_rules_install_mutation.ts | 25 +++- .../add_prebuilt_rules_header_buttons.tsx | 86 +++++++++++-- .../add_prebuilt_rules_install_button.tsx | 118 ++++++++++++++++++ .../add_prebuilt_rules_table_context.tsx | 82 +++++++----- .../add_prebuilt_rules_table/translations.ts | 29 +++++ .../use_add_prebuilt_rules_table_columns.tsx | 34 ++--- .../detection_engine/rules/translations.ts | 8 -- 7 files changed, 304 insertions(+), 78 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_install_button.tsx diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_install_mutation.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_install_mutation.ts index 7f7fab65b0d95..3b448219d6e01 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_install_mutation.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_install_mutation.ts @@ -16,8 +16,10 @@ import { useInvalidateFindRulesQuery } from '../use_find_rules_query'; import { useInvalidateFetchRuleManagementFiltersQuery } from '../use_fetch_rule_management_filters_query'; import { useInvalidateFetchRulesSnoozeSettingsQuery } from '../use_fetch_rules_snooze_settings_query'; import { useInvalidateFetchPrebuiltRulesInstallReviewQuery } from './use_fetch_prebuilt_rules_install_review_query'; +import type { BulkAction } from '../../api'; import { performInstallSpecificRules } from '../../api'; import { useInvalidateFetchCoverageOverviewQuery } from '../use_fetch_coverage_overview_query'; +import { useBulkActionMutation } from '../use_bulk_action_mutation'; export const PERFORM_SPECIFIC_RULES_INSTALLATION_KEY = [ 'POST', @@ -25,11 +27,16 @@ export const PERFORM_SPECIFIC_RULES_INSTALLATION_KEY = [ PERFORM_RULE_INSTALLATION_URL, ]; +export interface UsePerformSpecificRulesInstallParams { + rules: InstallSpecificRulesRequest['rules']; + enable?: boolean; +} + export const usePerformSpecificRulesInstallMutation = ( options?: UseMutationOptions< PerformRuleInstallationResponseBody, Error, - InstallSpecificRulesRequest['rules'] + UsePerformSpecificRulesInstallParams > ) => { const invalidateFindRulesQuery = useInvalidateFindRulesQuery(); @@ -40,15 +47,15 @@ export const usePerformSpecificRulesInstallMutation = ( useInvalidateFetchPrebuiltRulesInstallReviewQuery(); const invalidateRuleStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); const invalidateFetchCoverageOverviewQuery = useInvalidateFetchCoverageOverviewQuery(); + const { mutateAsync } = useBulkActionMutation(); return useMutation< PerformRuleInstallationResponseBody, Error, - InstallSpecificRulesRequest['rules'] + UsePerformSpecificRulesInstallParams >( - (rulesToInstall: InstallSpecificRulesRequest['rules']) => { - return performInstallSpecificRules(rulesToInstall); - }, + (rulesToInstall: UsePerformSpecificRulesInstallParams) => + performInstallSpecificRules(rulesToInstall.rules), { ...options, mutationKey: PERFORM_SPECIFIC_RULES_INSTALLATION_KEY, @@ -62,6 +69,14 @@ export const usePerformSpecificRulesInstallMutation = ( invalidateRuleStatus(); invalidateFetchCoverageOverviewQuery(); + const [response, , { enable }] = args; + + if (response && enable) { + const ruleIdsToEnable = response.results.created.map((rule) => rule.id); + const bulkAction: BulkAction = { type: 'enable', ids: ruleIdsToEnable }; + mutateAsync({ bulkAction }); + } + if (options?.onSettled) { options.onSettled(...args); } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx index b943022f5d53d..b4ff6ab29a3ff 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx @@ -5,8 +5,18 @@ * 2.0. */ -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; -import React from 'react'; +import { + EuiButton, + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiPopover, +} from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { useBoolean } from 'react-use'; import { useUserData } from '../../../../../detections/components/user_info'; import { useAddPrebuiltRulesTableContext } from './add_prebuilt_rules_table_context'; import * as i18n from './translations'; @@ -31,19 +41,69 @@ export const AddPrebuiltRulesHeaderButtons = () => { const isRuleInstalling = loadingRules.length > 0; const isRequestInProgress = isRuleInstalling || isRefetching || isUpgradingSecurityPackages; + const [isOverflowPopoverOpen, setOverflowPopover] = useBoolean(false); + + const onOverflowButtonClick = () => { + setOverflowPopover(!isOverflowPopoverOpen); + }; + + const closeOverflowPopover = useCallback(() => { + setOverflowPopover(false); + }, [setOverflowPopover]); + + const enableOnClick = useCallback(() => { + installSelectedRules(true); + closeOverflowPopover(); + }, [closeOverflowPopover, installSelectedRules]); + + const installOnClick = useCallback(() => { + installSelectedRules(); + }, [installSelectedRules]); + + const overflowItems = useMemo( + () => [ + + {i18n.INSTALL_AND_ENABLE_BUTTON_LABEL} + , + ], + [enableOnClick] + ); + return ( {shouldDisplayInstallSelectedRulesButton ? ( - - - {i18n.INSTALL_SELECTED_RULES(numberOfSelectedRules)} - {isRuleInstalling ? : undefined} - - + <> + + + {i18n.INSTALL_SELECTED_RULES(numberOfSelectedRules)} + {isRuleInstalling && } + + + + + } + isOpen={isOverflowPopoverOpen} + closePopover={closeOverflowPopover} + panelPaddingSize="s" + anchorPosition="downRight" + > + + + + ) : null} { aria-label={i18n.INSTALL_ALL_ARIA_LABEL} > {i18n.INSTALL_ALL} - {isRuleInstalling ? : undefined} + {isRuleInstalling && } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_install_button.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_install_button.tsx new file mode 100644 index 0000000000000..ea83efae768fa --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_install_button.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiPopover, +} from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { useBoolean } from 'react-use'; +import type { Rule } from '../../../../rule_management/logic'; +import type { RuleSignatureId } from '../../../../../../common/api/detection_engine'; +import type { AddPrebuiltRulesTableActions } from './add_prebuilt_rules_table_context'; +import * as i18n from './translations'; + +export interface PrebuiltRulesInstallButtonProps { + ruleId: RuleSignatureId; + record: Rule; + installOneRule: AddPrebuiltRulesTableActions['installOneRule']; + loadingRules: RuleSignatureId[]; + isDisabled: boolean; +} + +export const PrebuiltRulesInstallButton = ({ + ruleId, + record, + installOneRule, + loadingRules, + isDisabled, +}: PrebuiltRulesInstallButtonProps) => { + const isRuleInstalling = loadingRules.includes(ruleId); + const isInstallButtonDisabled = isRuleInstalling || isDisabled; + const [isPopoverOpen, setPopover] = useBoolean(false); + + const onOverflowButtonClick = useCallback(() => { + setPopover(!isPopoverOpen); + }, [isPopoverOpen, setPopover]); + + const closeOverflowPopover = useCallback(() => { + setPopover(false); + }, [setPopover]); + + const enableOnClick = useCallback(() => { + installOneRule(ruleId, true); + closeOverflowPopover(); + }, [closeOverflowPopover, installOneRule, ruleId]); + + const installOnClick = useCallback(() => { + installOneRule(ruleId); + }, [installOneRule, ruleId]); + + const overflowItems = useMemo( + () => [ + + {i18n.INSTALL_AND_ENABLE_BUTTON_LABEL} + , + ], + [enableOnClick] + ); + + const popoverButton = useMemo( + () => ( + + ), + [isInstallButtonDisabled, onOverflowButtonClick] + ); + + if (isRuleInstalling) { + return ( + + ); + } + return ( + + + + {i18n.INSTALL_BUTTON_LABEL} + + + + + + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx index 5450fc1f64a1c..14e539ec40ae1 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx @@ -7,7 +7,7 @@ import type { Dispatch, SetStateAction } from 'react'; import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; -import { EuiButton } from '@elastic/eui'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useUserData } from '../../../../../detections/components/user_info'; import { useFetchPrebuiltRulesStatusQuery } from '../../../../rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_status_query'; import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages'; @@ -75,9 +75,9 @@ export interface AddPrebuiltRulesTableState { export interface AddPrebuiltRulesTableActions { reFetchRules: () => void; - installOneRule: (ruleId: RuleSignatureId) => void; + installOneRule: (ruleId: RuleSignatureId, enable?: boolean) => void; installAllRules: () => void; - installSelectedRules: () => void; + installSelectedRules: (enable?: boolean) => void; setFilterOptions: Dispatch>; selectRules: (rules: RuleResponse[]) => void; openRulePreview: (ruleId: RuleSignatureId) => void; @@ -140,13 +140,16 @@ export const AddPrebuiltRulesTableContextProvider = ({ const filteredRules = useFilterPrebuiltRulesToInstall({ filterOptions, rules }); const installOneRule = useCallback( - async (ruleId: RuleSignatureId) => { + async (ruleId: RuleSignatureId, enable?: boolean) => { const rule = rules.find((r) => r.rule_id === ruleId); invariant(rule, `Rule with id ${ruleId} not found`); setLoadingRules((prev) => [...prev, ruleId]); try { - await installSpecificRulesRequest([{ rule_id: ruleId, version: rule.version }]); + await installSpecificRulesRequest({ + rules: [{ rule_id: ruleId, version: rule.version }], + enable, + }); } finally { setLoadingRules((prev) => prev.filter((id) => id !== ruleId)); } @@ -154,19 +157,24 @@ export const AddPrebuiltRulesTableContextProvider = ({ [installSpecificRulesRequest, rules] ); - const installSelectedRules = useCallback(async () => { - const rulesToUpgrade = selectedRules.map((rule) => ({ - rule_id: rule.rule_id, - version: rule.version, - })); - setLoadingRules((prev) => [...prev, ...rulesToUpgrade.map((r) => r.rule_id)]); - try { - await installSpecificRulesRequest(rulesToUpgrade); - } finally { - setLoadingRules((prev) => prev.filter((id) => !rulesToUpgrade.some((r) => r.rule_id === id))); - setSelectedRules([]); - } - }, [installSpecificRulesRequest, selectedRules]); + const installSelectedRules = useCallback( + async (enable?: boolean) => { + const rulesToUpgrade = selectedRules.map((rule) => ({ + rule_id: rule.rule_id, + version: rule.version, + })); + setLoadingRules((prev) => [...prev, ...rulesToUpgrade.map((r) => r.rule_id)]); + try { + await installSpecificRulesRequest({ rules: rulesToUpgrade, enable }); + } finally { + setLoadingRules((prev) => + prev.filter((id) => !rulesToUpgrade.some((r) => r.rule_id === id)) + ); + setSelectedRules([]); + } + }, + [installSpecificRulesRequest, selectedRules] + ); const installAllRules = useCallback(async () => { // Unselect all rules so that the table doesn't show the "bulk actions" bar @@ -188,17 +196,33 @@ export const AddPrebuiltRulesTableContextProvider = ({ !(isPreviewRuleLoading || isRefetching || isUpgradingSecurityPackages); return ( - { - installOneRule(rule.rule_id); - closeRulePreview(); - }} - fill - data-test-subj="installPrebuiltRuleFromFlyoutButton" - > - {i18n.INSTALL_BUTTON_LABEL} - + + + { + installOneRule(rule.rule_id); + closeRulePreview(); + }} + data-test-subj="installPrebuiltRuleFromFlyoutButton" + > + {i18n.INSTALL_WITHOUT_ENABLING_BUTTON_LABEL} + + + + { + installOneRule(rule.rule_id, true); + closeRulePreview(); + }} + fill + data-test-subj="installAndEnablePrebuiltRuleFromFlyoutButton" + > + {i18n.INSTALL_AND_ENABLE_BUTTON_LABEL} + + + ); }, [ diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/translations.ts index a3ea514571151..c335f7624afd8 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/translations.ts @@ -44,3 +44,32 @@ export const INSTALL_BUTTON_LABEL = i18n.translate( defaultMessage: 'Install', } ); + +export const INSTALL_WITHOUT_ENABLING_BUTTON_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.installWithoutEnablingButtonLabel', + { + defaultMessage: 'Install without enabling', + } +); + +export const INSTALL_AND_ENABLE_BUTTON_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.installAndEnableButtonLabel', + { + defaultMessage: 'Install and enable', + } +); + +export const INSTALL_RULE_BUTTON_ARIA_LABEL = (ruleName: string) => + i18n.translate('xpack.securitySolution.addRules.installRuleButton.ariaLabel', { + defaultMessage: 'Install "{ruleName}"', + values: { + ruleName, + }, + }); + +export const INSTALL_RULES_OVERFLOW_BUTTON_ARIA_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.installOverflowButton.ariaLabel', + { + defaultMessage: 'More install options', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/use_add_prebuilt_rules_table_columns.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/use_add_prebuilt_rules_table_columns.tsx index 70c40349fc80c..eaf3af79ee360 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/use_add_prebuilt_rules_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/use_add_prebuilt_rules_table_columns.tsx @@ -6,7 +6,7 @@ */ import type { EuiBasicTableColumn } from '@elastic/eui'; -import { EuiButtonEmpty, EuiBadge, EuiText, EuiLoadingSpinner, EuiLink } from '@elastic/eui'; +import { EuiBadge, EuiText, EuiLink } from '@elastic/eui'; import React, { useMemo } from 'react'; import { RulesTableEmptyColumnName } from '../rules_table_empty_column_name'; import { SHOW_RELATED_INTEGRATIONS_SETTING } from '../../../../../../common/constants'; @@ -25,6 +25,7 @@ import type { RuleResponse, } from '../../../../../../common/api/detection_engine/model/rule_schema'; import { getNormalizedSeverity } from '../helpers'; +import { PrebuiltRulesInstallButton } from './add_prebuilt_rules_install_button'; export type TableColumn = EuiBasicTableColumn; @@ -113,28 +114,15 @@ const createInstallButtonColumn = ( ): TableColumn => ({ field: 'rule_id', name: , - render: (ruleId: RuleSignatureId, record: Rule) => { - const isRuleInstalling = loadingRules.includes(ruleId); - const isInstallButtonDisabled = isRuleInstalling || isDisabled; - return ( - installOneRule(ruleId)} - data-test-subj={`installSinglePrebuiltRuleButton-${ruleId}`} - aria-label={i18n.INSTALL_RULE_BUTTON_ARIA_LABEL(record.name)} - > - {isRuleInstalling ? ( - - ) : ( - i18n.INSTALL_RULE_BUTTON - )} - - ); - }, + render: (ruleId: RuleSignatureId, record: Rule) => ( + + ), width: '10%', align: 'center', }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index ca42502d93c4e..b573edd84343f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -1393,14 +1393,6 @@ export const INSTALL_RULE_BUTTON = i18n.translate( } ); -export const INSTALL_RULE_BUTTON_ARIA_LABEL = (ruleName: string) => - i18n.translate('xpack.securitySolution.addRules.installRuleButton.ariaLabel', { - defaultMessage: 'Install "{ruleName}"', - values: { - ruleName, - }, - }); - export const UPDATE_RULE_BUTTON = i18n.translate( 'xpack.securitySolution.addRules.upgradeRuleButton', { From 398a7583da87096e15398f974ae6c5a8aaffbcd8 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 19 Sep 2024 22:51:06 +1000 Subject: [PATCH 06/19] [8.x] [Console] Update Elasticsearch specification metamodel (#193239) (#193405) # Backport This will backport the following commits from `main` to `8.x`: - [[Console] Update Elasticsearch specification metamodel (#193239)](https://github.com/elastic/kibana/pull/193239) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Quentin Pradet --- .../src/types/specification_types.ts | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/packages/kbn-generate-console-definitions/src/types/specification_types.ts b/packages/kbn-generate-console-definitions/src/types/specification_types.ts index a3816133827bf..fc2f694290801 100644 --- a/packages/kbn-generate-console-definitions/src/types/specification_types.ts +++ b/packages/kbn-generate-console-definitions/src/types/specification_types.ts @@ -92,7 +92,7 @@ export interface DictionaryOf { } /** - * A user defined value. To be used when bubbling a generic parameter up to the top-level interface is + * A user defined value. To be used when bubbling a generic parameter up to the top-level class is * inconvenient or impossible (e.g. for lists of user-defined values of possibly different types). * * Clients will allow providing a serializer/deserializer when reading/writing properties of this type, @@ -139,7 +139,7 @@ export interface Property { codegenName?: string; /** An optional set of aliases for `name` */ aliases?: string[]; - /** If the enclosing interface is a variants container, is this a property of the container and not a variant? */ + /** If the enclosing class is a variants container, is this a property of the container and not a variant? */ containerProperty?: boolean; /** If this property has a quirk that needs special attention, give a short explanation about it */ esQuirk?: string; @@ -181,7 +181,7 @@ export interface BaseType { specLocation: string; } -export type Variants = ExternalTag | InternalTag | Container; +export type Variants = ExternalTag | InternalTag | Container | Untagged; export interface VariantBase { /** @@ -208,6 +208,11 @@ export interface Container extends VariantBase { kind: 'container'; } +export interface Untagged extends VariantBase { + kind: 'untagged'; + untypedVariant: TypeName; +} + /** * Inherits clause (aka extends or implements) for an interface or request */ @@ -216,6 +221,12 @@ export interface Inherits { generics?: ValueOf[]; } +export interface Behavior { + type: TypeName; + generics?: ValueOf[]; + meta?: Record; +} + /** * An interface type */ @@ -232,7 +243,7 @@ export interface Interface extends BaseType { /** * Behaviors directly implemented by this interface */ - behaviors?: Inherits[]; + behaviors?: Behavior[]; /** * Behaviors attached to this interface, coming from the interface itself (see `behaviors`) @@ -271,12 +282,12 @@ export interface Request extends BaseType { // We can also pull path parameter descriptions on body properties they replace /** - * Body type. Most often a list of properties (that can extend those of the inherited interface, see above), except for a + * Body type. Most often a list of properties (that can extend those of the inherited class, see above), except for a * few specific cases that use other types such as bulk (array) or create (generic parameter). Or NoBody for requests * that don't have a body. */ body: Body; - behaviors?: Inherits[]; + behaviors?: Behavior[]; attachedBehaviors?: string[]; } @@ -287,7 +298,7 @@ export interface Response extends BaseType { kind: 'response'; generics?: TypeName[]; body: Body; - behaviors?: Inherits[]; + behaviors?: Behavior[]; attachedBehaviors?: string[]; exceptions?: ResponseException[]; } @@ -335,6 +346,7 @@ export interface EnumMember { description?: string; deprecation?: Deprecation; since?: string; + availability?: Availabilities; } /** @@ -358,8 +370,11 @@ export interface TypeAlias extends BaseType { type: ValueOf; /** generic parameters: either concrete types or open parameters from the enclosing type */ generics?: TypeName[]; - /** Only applicable to `union_of` aliases: identify typed_key unions (external) and variant inventories (internal) */ - variants?: InternalTag | ExternalTag; + /** + * Only applicable to `union_of` aliases: identify typed_key unions (external), variant inventories (internal) + * and untagged variants + */ + variants?: InternalTag | ExternalTag | Untagged; } // ------------------------------------------------------------------------------------------------ @@ -438,6 +453,14 @@ export interface UrlTemplate { } export interface Model { + _info?: { + title: string; + license: { + name: string; + url: string; + }; + }; + types: TypeDefinition[]; endpoints: Endpoint[]; } From 21582a7ff5540b92d65f6b5a0f1fb2f0fefd254e Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 19 Sep 2024 23:10:51 +1000 Subject: [PATCH 07/19] [8.x] [Reporting] update puppeteer to version 23.3.1 (#192345) (#193357) # Backport This will backport the following commits from `main` to `8.x`: - [[Reporting] update puppeteer to version 23.3.1 (#192345)](https://github.com/elastic/kibana/pull/192345) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) \r\n\r\n\r\n### How to verify linux headless build\r\n- clone the following repo\r\nhttps://github.com/tsullivan/kibana-dev-docker\r\n- pull this particular PR\r\n- follow the steps outlined in the repo, replacing any occurrence of\r\n`kibana--SNAPSHOT-linux-aarch64.tar.gz` from the repo above's\r\nstep with the output of running build on this changeset.\r\n- before running step 4, modify the `kibana.yml` file from the\r\n`kibana-dev-docker` repo and include the following so we might be able\r\nto verify the version of chromium running;\r\n ```yaml\r\n logging.loggers:\r\n - name: plugins.reporting\r\n level: debug\r\n ```\r\n- complete the steps outlined in the README, you'll have a linux distro\r\nof kibana running on port `5601`\r\n- Attempt creating exports of PDF and PNG reports, in dashboard, canvas,\r\nand visualizations, on report creation attempt we would see a log output\r\nthat prints out the chromium version exactly matching this;\r\n\r\n\"Screenshot\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Elastic Machine ","sha":"91ca8ab95c01f88ea2d79779f3670c88bf269da0","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:SharedUX","backport:prev-major","v8.16.0","backport:version","v8.15.2"],"title":"[Reporting] update puppeteer to version 23.3.1","number":192345,"url":"https://github.com/elastic/kibana/pull/192345","mergeCommit":{"message":"[Reporting] update puppeteer to version 23.3.1 (#192345)\n\n## Summary\r\n\r\nUpdate for puppeteer, the following changeset updates puppeteer to\r\nversion `23.3.1`.\r\n\r\nThe chromium version required for this version of puppeteer is\r\n`128.0.6613.137` from revision `1331488`, as such the chromium binary\r\nincluded for windows and darwin platforms either match or were the\r\nclosest revision to the expectation. The linux headless binary was built\r\nfrom commit `fe621c5aa2d6b987e964fb1b5066833da5fb613d` of the same\r\nrevision.\r\n\r\n_**N.B.**_ Puppeteer 23.0.0 is earmarked as containing breaking changes\r\nsee\r\n[here](https://github.com/puppeteer/puppeteer/blob/abda5dcc9912f4fa2c5a566403108db783f48538/packages/puppeteer-core/CHANGELOG.md#2300-2024-08-07),\r\nthis PR considers the outlined changes and makes relevant adjustments so\r\nreporting continues working as is.\r\n\r\n\r\n\r\n\r\n### How to verify linux headless build\r\n- clone the following repo\r\nhttps://github.com/tsullivan/kibana-dev-docker\r\n- pull this particular PR\r\n- follow the steps outlined in the repo, replacing any occurrence of\r\n`kibana--SNAPSHOT-linux-aarch64.tar.gz` from the repo above's\r\nstep with the output of running build on this changeset.\r\n- before running step 4, modify the `kibana.yml` file from the\r\n`kibana-dev-docker` repo and include the following so we might be able\r\nto verify the version of chromium running;\r\n ```yaml\r\n logging.loggers:\r\n - name: plugins.reporting\r\n level: debug\r\n ```\r\n- complete the steps outlined in the README, you'll have a linux distro\r\nof kibana running on port `5601`\r\n- Attempt creating exports of PDF and PNG reports, in dashboard, canvas,\r\nand visualizations, on report creation attempt we would see a log output\r\nthat prints out the chromium version exactly matching this;\r\n\r\n\"Screenshot\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Elastic Machine ","sha":"91ca8ab95c01f88ea2d79779f3670c88bf269da0"}},"sourceBranch":"main","suggestedTargetBranches":["8.x","8.15"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/192345","number":192345,"mergeCommit":{"message":"[Reporting] update puppeteer to version 23.3.1 (#192345)\n\n## Summary\r\n\r\nUpdate for puppeteer, the following changeset updates puppeteer to\r\nversion `23.3.1`.\r\n\r\nThe chromium version required for this version of puppeteer is\r\n`128.0.6613.137` from revision `1331488`, as such the chromium binary\r\nincluded for windows and darwin platforms either match or were the\r\nclosest revision to the expectation. The linux headless binary was built\r\nfrom commit `fe621c5aa2d6b987e964fb1b5066833da5fb613d` of the same\r\nrevision.\r\n\r\n_**N.B.**_ Puppeteer 23.0.0 is earmarked as containing breaking changes\r\nsee\r\n[here](https://github.com/puppeteer/puppeteer/blob/abda5dcc9912f4fa2c5a566403108db783f48538/packages/puppeteer-core/CHANGELOG.md#2300-2024-08-07),\r\nthis PR considers the outlined changes and makes relevant adjustments so\r\nreporting continues working as is.\r\n\r\n\r\n\r\n\r\n### How to verify linux headless build\r\n- clone the following repo\r\nhttps://github.com/tsullivan/kibana-dev-docker\r\n- pull this particular PR\r\n- follow the steps outlined in the repo, replacing any occurrence of\r\n`kibana--SNAPSHOT-linux-aarch64.tar.gz` from the repo above's\r\nstep with the output of running build on this changeset.\r\n- before running step 4, modify the `kibana.yml` file from the\r\n`kibana-dev-docker` repo and include the following so we might be able\r\nto verify the version of chromium running;\r\n ```yaml\r\n logging.loggers:\r\n - name: plugins.reporting\r\n level: debug\r\n ```\r\n- complete the steps outlined in the README, you'll have a linux distro\r\nof kibana running on port `5601`\r\n- Attempt creating exports of PDF and PNG reports, in dashboard, canvas,\r\nand visualizations, on report creation attempt we would see a log output\r\nthat prints out the chromium version exactly matching this;\r\n\r\n\"Screenshot\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Elastic Machine ","sha":"91ca8ab95c01f88ea2d79779f3670c88bf269da0"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.15","label":"v8.15.2","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Eyo O. Eyo <7893459+eokoneyo@users.noreply.github.com> Co-authored-by: Elastic Machine --- package.json | 2 +- .../kbn-screenshotting-server/src/paths.ts | 34 ++++----- x-pack/build_chromium/linux/args.gn | 2 +- .../server/browsers/chromium/driver.ts | 25 +++--- .../browsers/chromium/driver_factory/index.ts | 2 +- yarn.lock | 76 ++++++++++--------- 6 files changed, 76 insertions(+), 65 deletions(-) diff --git a/package.json b/package.json index c963505e2ee72..431275f8aa694 100644 --- a/package.json +++ b/package.json @@ -1173,7 +1173,7 @@ "pretty-ms": "6.0.0", "prop-types": "^15.8.1", "proxy-from-env": "1.0.0", - "puppeteer": "22.13.1", + "puppeteer": "23.3.1", "query-string": "^6.13.2", "rbush": "^3.0.1", "re-resizable": "^6.9.9", diff --git a/packages/kbn-screenshotting-server/src/paths.ts b/packages/kbn-screenshotting-server/src/paths.ts index f125ba1b74860..9e8200c0839ab 100644 --- a/packages/kbn-screenshotting-server/src/paths.ts +++ b/packages/kbn-screenshotting-server/src/paths.ts @@ -46,10 +46,10 @@ export class ChromiumArchivePaths { platform: 'darwin', architecture: 'x64', archiveFilename: 'chrome-mac.zip', - archiveChecksum: 'fa8004f3c8c5574c089c901e48429d1b01720bf3dd25e05ac56c41d0ab470c10', - binaryChecksum: '56f25cb6881e5c2b1aac0d8e87630517d1af8effdc9319d35f872add048df1ca', + archiveChecksum: '0a3d18efd00b3406f66139a673616b4b2b4b00323776678cb82295996f5a6733', + binaryChecksum: '8bcdaa973ee11110f6b70eaac2418fda3bb64446cf37f964fce331cdc8907a20', binaryRelativePath: 'chrome-mac/Chromium.app/Contents/MacOS/Chromium', - revision: 1300317, // 1300313 is not available for Mac_x64 + revision: 1331485, // 1331488 is not available for Mac_x64 location: 'common', archivePath: 'Mac', isPreInstalled: false, @@ -58,10 +58,10 @@ export class ChromiumArchivePaths { platform: 'darwin', architecture: 'arm64', archiveFilename: 'chrome-mac.zip', - archiveChecksum: 'bea49fd3ccd6aaccd7cdc4df38306f002a2934aaa2c044f3b5a3272b31ec77ca', - binaryChecksum: '4c55d9e47deb1179c377c9785afdcdb5f3d3f351bff62b414d43e32ff195bd55', + archiveChecksum: '426eddf16acb88b9446a91de53cc4364c7d487414248f33e30f68cf488cea0c0', + binaryChecksum: '827931739bfdd2b6790a81d5ade8886c159cd051581d79b84d1ede447293e9cf', binaryRelativePath: 'chrome-mac/Chromium.app/Contents/MacOS/Chromium', - revision: 1300314, // 1300313 is not available for Mac_Arm + revision: 1331488, location: 'common', archivePath: 'Mac_Arm', isPreInstalled: false, @@ -69,22 +69,22 @@ export class ChromiumArchivePaths { { platform: 'linux', architecture: 'x64', - archiveFilename: 'chromium-5b5d829-locales-linux_x64.zip', - archiveChecksum: '799e8fd5f47ea70b8a3972d39b2617c9cbebc7fc433a89251dae312a7c77534b', - binaryChecksum: '216b8f7ff9b41e985397342c2df54e4f8e07a01a3b8a929f39b9a10931d26ff5', + archiveFilename: 'chromium-fe621c5-locales-linux_x64.zip', + archiveChecksum: '12ce2e0eac184072dfcbc7a267328e3eb7fbe10a682997f4111c0378f2397341', + binaryChecksum: '670481cfa8db209401106cd23051009d390c03608724d0822a12c8c0a92b4c25', binaryRelativePath: 'headless_shell-linux_x64/headless_shell', - revision: 1300313, + revision: 1331488, location: 'custom', isPreInstalled: true, }, { platform: 'linux', architecture: 'arm64', - archiveFilename: 'chromium-5b5d829-locales-linux_arm64.zip', - archiveChecksum: '961e20c45c61f8e948efdc4128bb17c23217bbcb28537f270ccf5bf0826981e7', - binaryChecksum: 'fc4027fb6b1c96bef9374d5d9f791097fae2ec2ddc4e0134167075bd52d1458f', + archiveFilename: 'chromium-fe621c5-locales-linux_arm64.zip', + archiveChecksum: 'f7333eaff5235046c8775f0c1a0b7395b7ebc2e054ea638710cf511c4b6f9daf', + binaryChecksum: '8a3a3371b3d04f4b0880b137a3611c223e0d8e65a218943cb7be1ec4a91f5e35', binaryRelativePath: 'headless_shell-linux_arm64/headless_shell', - revision: 1300313, + revision: 1331488, location: 'custom', isPreInstalled: true, }, @@ -92,10 +92,10 @@ export class ChromiumArchivePaths { platform: 'win32', architecture: 'x64', archiveFilename: 'chrome-win.zip', - archiveChecksum: '27a2ed1473cefc6f48ff5665faa1fbcc69ef5be47ee21777a60e87c8379fdd93', - binaryChecksum: 'd603401a5e6f8bd734b329876e4221a4d24a1999f14df6e32eeb5e6a72520d96', + archiveChecksum: 'fa62be702f55f37e455bab4291c59ceb40e81e1922d30cf9453a4ee176b909bc', + binaryChecksum: '1345e66583bad1a1f16885f381d1173de8bf931487da9ba155e1b58bf23b2c66', binaryRelativePath: path.join('chrome-win', 'chrome.exe'), - revision: 1300320, // 1300313 is not available for win32 + revision: 1331487, // 1331488 is not available for win32 location: 'common', archivePath: 'Win', isPreInstalled: true, diff --git a/x-pack/build_chromium/linux/args.gn b/x-pack/build_chromium/linux/args.gn index 01af3bf766f7b..a7d5713b92cd0 100644 --- a/x-pack/build_chromium/linux/args.gn +++ b/x-pack/build_chromium/linux/args.gn @@ -17,7 +17,7 @@ v8_symbol_level = 0 enable_ink = false rtc_build_examples = false angle_build_tests = false -enable_screen_ai_service = false +use_fake_screen_ai = true enable_vr = false # Please, consult @elastic/kibana-security before changing/removing this option. diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts index 02d611f9ca00f..3c199b2916b30 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts @@ -244,15 +244,17 @@ export class HeadlessChromiumDriver { if (error) { await this.injectScreenshottingErrorHeader(error, getPrintLayoutSelectors().screenshot); } - return this.page.pdf({ - format: 'a4', - preferCSSPageSize: true, - scale: 1, - landscape: false, - displayHeaderFooter: true, - headerTemplate: await getHeaderTemplate({ title }), - footerTemplate: await getFooterTemplate({ logo }), - }); + return Buffer.from( + await this.page.pdf({ + format: 'a4', + preferCSSPageSize: true, + scale: 1, + landscape: false, + displayHeaderFooter: true, + headerTemplate: await getHeaderTemplate({ title }), + footerTemplate: await getFooterTemplate({ logo }), + }) + ); } /* @@ -272,6 +274,7 @@ export class HeadlessChromiumDriver { } const { boundingClientRect, scroll } = elementPosition; + const screenshot = await this.page.screenshot({ clip: { x: boundingClientRect.left + scroll.x, @@ -282,8 +285,8 @@ export class HeadlessChromiumDriver { captureBeyondViewport: false, // workaround for an internal resize. See: https://github.com/puppeteer/puppeteer/issues/7043 }); - if (Buffer.isBuffer(screenshot)) { - return screenshot; + if (screenshot.byteLength) { + return Buffer.from(screenshot); } if (typeof screenshot === 'string') { diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts index f6015c319cc0a..d8503b70ad963 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts @@ -150,7 +150,7 @@ export class HeadlessChromiumDriverFactory { pipe: !this.config.browser.chromium.inspect, userDataDir: this.userDataDir, executablePath: this.binaryPath, - ignoreHTTPSErrors: true, + acceptInsecureCerts: true, handleSIGHUP: false, args: chromiumArgs, defaultViewport: viewport, diff --git a/yarn.lock b/yarn.lock index 2272801ea9b6c..bee0b7b7f8443 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8310,16 +8310,16 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= -"@puppeteer/browsers@2.2.4": - version "2.2.4" - resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-2.2.4.tgz#4307245d881aa5a79743050be66568bad0f6ffbb" - integrity sha512-BdG2qiI1dn89OTUUsx2GZSpUzW+DRffR1wlMJyKxVHYrhnKoELSDxDd+2XImUkuWPEKk76H5FcM/gPFrEK1Tfw== +"@puppeteer/browsers@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-2.4.0.tgz#a0dd0f4e381e53f509109ae83b891db5972750f5" + integrity sha512-x8J1csfIygOwf6D6qUAZ0ASk3z63zPb7wkNeHRerCMh82qWKUrOgkuP005AJC8lDL6/evtXETGEJVcwykKT4/g== dependencies: - debug "^4.3.5" + debug "^4.3.6" extract-zip "^2.0.1" progress "^2.0.3" proxy-agent "^6.4.0" - semver "^7.6.2" + semver "^7.6.3" tar-fs "^3.0.6" unbzip2-stream "^1.4.3" yargs "^17.7.2" @@ -14199,10 +14199,10 @@ chromedriver@^128.0.1: proxy-from-env "^1.1.0" tcp-port-used "^1.0.2" -chromium-bidi@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-0.6.1.tgz#533612dd166b7b36a8ba8b90685ad2fa0c98d064" - integrity sha512-kSxJRj0VgtUKz6nmzc2JPfyfJGzwzt65u7PqhPHtgGQUZLF5oG+ST6l6e5ONfStUMAlhSutFCjaGKllXZa16jA== +chromium-bidi@0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-0.6.5.tgz#31be98f9ee5c93fa99d240c680518c9293d8c6bb" + integrity sha512-RuLrmzYrxSb0s9SgpB+QN5jJucPduZQ/9SIe76MDxYJuecPW5mxMdacJ1f4EtgiV+R0p3sCkznTMvH0MPGFqjA== dependencies: mitt "3.0.1" urlpattern-polyfill "10.0.0" @@ -15763,12 +15763,12 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.5: - version "4.3.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" - integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== +debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.5, debug@^4.3.6, debug@^4.3.7: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== dependencies: - ms "2.1.2" + ms "^2.1.3" debug@4.3.1: version "4.3.1" @@ -16229,10 +16229,10 @@ detective@^5.0.2: defined "^1.0.0" minimist "^1.1.1" -devtools-protocol@0.0.1299070: - version "0.0.1299070" - resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1299070.tgz#b3e4cf0b678a46f0f907ae6e07e03ad3a53c00df" - integrity sha512-+qtL3eX50qsJ7c+qVyagqi7AWMoQCBGNfoyJZMwm/NSXVqLYbuitrWEEIzxfUmTNy7//Xe8yhMmQ+elj3uAqSg== +devtools-protocol@0.0.1330662: + version "0.0.1330662" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1330662.tgz#400fe703c2820d6b2d9ebdd1785934310152373e" + integrity sha512-pzh6YQ8zZfz3iKlCvgzVCu22NdpZ8hNmwU6WnQjNVquh0A9iVosPtNLWDwaWVGyrntQlltPFztTMK5Cg6lfCuw== dezalgo@^1.0.0, dezalgo@^1.0.4: version "1.0.4" @@ -26273,26 +26273,29 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -puppeteer-core@22.13.1: - version "22.13.1" - resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-22.13.1.tgz#3ba03e5ebd98bbbd86e465864cf00314e07309de" - integrity sha512-NmhnASYp51QPRCAf9n0OPxuPMmzkKd8+2sB9Q+BjwwCG25gz6iuNc3LQDWa+cH2tyivmJppLhNNFt6Q3HmoOpw== +puppeteer-core@23.3.1: + version "23.3.1" + resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-23.3.1.tgz#b93d825e586f5f7dc268128a31a31c62bbe378ae" + integrity sha512-m5gTpITEqqpSgAvPUI/Ch9igh5sNJV+BVVbqQMzqirRDVHDCkLGHaydEQZx2NZvSXdwCFrIV///cpSlX/uD0Sg== dependencies: - "@puppeteer/browsers" "2.2.4" - chromium-bidi "0.6.1" - debug "^4.3.5" - devtools-protocol "0.0.1299070" + "@puppeteer/browsers" "2.4.0" + chromium-bidi "0.6.5" + debug "^4.3.7" + devtools-protocol "0.0.1330662" + typed-query-selector "^2.12.0" ws "^8.18.0" -puppeteer@22.13.1: - version "22.13.1" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-22.13.1.tgz#f8e4217919b438f18adb754e9d8414fef58fb3de" - integrity sha512-PwXLDQK5u83Fm5A7TGMq+9BR7iHDJ8a3h21PSsh/E6VfhxiKYkU7+tvGZNSCap6k3pCNDd9oNteVBEctcBalmQ== +puppeteer@23.3.1: + version "23.3.1" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-23.3.1.tgz#830ac4b2c264ae4a610b79be77aff23bb13efa2c" + integrity sha512-BxkuJyCv46ZKW8KEHiVMHgHEC89jKK9FffReWjbw1IfBUmNx+6JIZyqOtaJeSwyolTdVqqb5fiPiXflKeH3dKQ== dependencies: - "@puppeteer/browsers" "2.2.4" + "@puppeteer/browsers" "2.4.0" + chromium-bidi "0.6.5" cosmiconfig "^9.0.0" - devtools-protocol "0.0.1299070" - puppeteer-core "22.13.1" + devtools-protocol "0.0.1330662" + puppeteer-core "23.3.1" + typed-query-selector "^2.12.0" pure-rand@^6.0.0: version "6.0.2" @@ -30878,6 +30881,11 @@ typed-array-length@^1.0.6: is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" +typed-query-selector@^2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/typed-query-selector/-/typed-query-selector-2.12.0.tgz#92b65dbc0a42655fccf4aeb1a08b1dddce8af5f2" + integrity sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg== + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" From 014add7e2f0982f01ec1e96e44e4c0006a07fdac Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 19 Sep 2024 23:34:28 +1000 Subject: [PATCH 08/19] [8.x] [Security Solution] Make retry messages in `retryIfDeleteByQueryConflicts` loggable (#193117) (#193410) # Backport This will backport the following commits from `main` to `8.x`: - [[Security Solution] Make retry messages in `retryIfDeleteByQueryConflicts` loggable (#193117)](https://github.com/elastic/kibana/pull/193117) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Maxim Palenov --- .../utils/retry_delete_by_query_conflicts.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/retry_delete_by_query_conflicts.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/retry_delete_by_query_conflicts.ts index de7e1afd163c9..e7710e2c21ad9 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/retry_delete_by_query_conflicts.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/retry_delete_by_query_conflicts.ts @@ -29,13 +29,12 @@ export async function retryIfDeleteByQueryConflicts( const operationResult = await operation(); if (!operationResult.failures || operationResult.failures?.length === 0) { - logger.info(`${name} finished successfully`); return operationResult; } const failureCause = operationResult.failures.map((failure) => failure.cause).join(', '); - logger.warning(`Unable to delete by query ${name}. Caused by: "${failureCause}". Retrying ...`); + logger.error(`Unable to delete by query ${name}. Caused by: "${failureCause}". Retrying ...`); await waitBeforeNextRetry(retryDelay); } From 33e06d21e77a8bc50db0cc5cb23658d26e734e69 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 19 Sep 2024 16:56:18 +0300 Subject: [PATCH 09/19] [8.x] [ResponseOps][Cases] Fix a bug with cases telemetry where data from other spaces are not included (#193166) (#193417) # Backport This will backport the following commits from `main` to `8.x`: - [[ResponseOps][Cases] Fix a bug with cases telemetry where data from other spaces are not included (#193166)](https://github.com/elastic/kibana/pull/193166) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --- .../telemetry/collect_telemetry_data.ts | 2 +- .../plugins/cases/server/telemetry/index.ts | 17 +++--- .../server/telemetry/queries/alerts.test.ts | 11 +++- .../queries/case_system_action.test.ts | 50 ++++++++++++++++- .../telemetry/queries/case_system_action.ts | 1 + .../server/telemetry/queries/cases.test.ts | 21 +++++++- .../cases/server/telemetry/queries/cases.ts | 19 ++++--- .../server/telemetry/queries/comments.test.ts | 14 ++++- .../telemetry/queries/configuration.test.ts | 24 +++++++-- .../server/telemetry/queries/configuration.ts | 1 + .../telemetry/queries/connectors.test.ts | 12 ++++- .../server/telemetry/queries/connectors.ts | 1 + .../queries/{pushed.test.ts => push.test.ts} | 15 ++++-- .../telemetry/queries/{pushes.ts => push.ts} | 1 + .../telemetry/queries/user_actions.test.ts | 14 ++++- .../server/telemetry/queries/utils.test.ts | 23 ++++++-- .../cases/server/telemetry/queries/utils.ts | 5 +- .../telemetry_saved_objects_client.test.ts | 30 +++++++++++ .../telemetry_saved_objects_client.ts | 24 +++++++++ .../plugins/cases/server/telemetry/types.ts | 5 +- .../common/lib/api/index.ts | 2 + .../common/lib/api/telemetry.ts | 38 +++++++++++++ .../common/plugins/cases/kibana.jsonc | 1 + .../common/plugins/cases/server/plugin.ts | 2 + .../common/plugins/cases/server/routes.ts | 29 ++++++++++ .../common/plugins/cases/tsconfig.json | 1 + .../security_and_spaces/tests/common/index.ts | 5 ++ .../tests/common/telemetry.ts | 53 +++++++++++++++++++ 28 files changed, 379 insertions(+), 42 deletions(-) rename x-pack/plugins/cases/server/telemetry/queries/{pushed.test.ts => push.test.ts} (82%) rename x-pack/plugins/cases/server/telemetry/queries/{pushes.ts => push.ts} (98%) create mode 100644 x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.test.ts create mode 100644 x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.ts create mode 100644 x-pack/test/cases_api_integration/common/lib/api/telemetry.ts create mode 100644 x-pack/test/cases_api_integration/security_and_spaces/tests/common/telemetry.ts diff --git a/x-pack/plugins/cases/server/telemetry/collect_telemetry_data.ts b/x-pack/plugins/cases/server/telemetry/collect_telemetry_data.ts index cabb7743a540d..1bcf599f014fb 100644 --- a/x-pack/plugins/cases/server/telemetry/collect_telemetry_data.ts +++ b/x-pack/plugins/cases/server/telemetry/collect_telemetry_data.ts @@ -11,7 +11,7 @@ import { getCasesSystemActionData } from './queries/case_system_action'; import { getUserCommentsTelemetryData } from './queries/comments'; import { getConfigurationTelemetryData } from './queries/configuration'; import { getConnectorsTelemetryData } from './queries/connectors'; -import { getPushedTelemetryData } from './queries/pushes'; +import { getPushedTelemetryData } from './queries/push'; import { getUserActionsTelemetryData } from './queries/user_actions'; import type { CasesTelemetry, CollectTelemetryDataParams } from './types'; diff --git a/x-pack/plugins/cases/server/telemetry/index.ts b/x-pack/plugins/cases/server/telemetry/index.ts index 5f10dcc6a3c72..c30d34d6c215c 100644 --- a/x-pack/plugins/cases/server/telemetry/index.ts +++ b/x-pack/plugins/cases/server/telemetry/index.ts @@ -5,12 +5,7 @@ * 2.0. */ -import type { - CoreSetup, - ISavedObjectsRepository, - Logger, - PluginInitializerContext, -} from '@kbn/core/server'; +import type { CoreSetup, Logger, PluginInitializerContext } from '@kbn/core/server'; import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import type { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; @@ -25,6 +20,7 @@ import { } from '../../common/constants'; import type { CasesTelemetry } from './types'; import { casesSchema } from './schema'; +import { TelemetrySavedObjectsClient } from './telemetry_saved_objects_client'; export { scheduleCasesTelemetryTask } from './schedule_telemetry_task'; @@ -42,13 +38,18 @@ export const createCasesTelemetry = ({ usageCollection, logger, }: CreateCasesTelemetryArgs) => { - const getInternalSavedObjectClient = async (): Promise => { + const getInternalSavedObjectClient = async (): Promise => { const [coreStart] = await core.getStartServices(); - return coreStart.savedObjects.createInternalRepository([ + const soClient = coreStart.savedObjects.createInternalRepository([ ...SAVED_OBJECT_TYPES, FILE_SO_TYPE, CASE_RULES_SAVED_OBJECT, ]); + + // Wrapping the internalRepository with the `TelemetrySavedObjectsClient` + // to ensure some best practices when collecting "all the telemetry" + // (i.e.: `.find` requests should query all spaces) + return new TelemetrySavedObjectsClient(soClient); }; taskManager.registerTaskDefinitions({ diff --git a/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts b/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts index 0eaa99c57c0f3..11636b50ebd4e 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts @@ -7,12 +7,15 @@ import { loggingSystemMock, savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; import { getAlertsTelemetryData } from './alerts'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('alerts', () => { const logger = loggingSystemMock.createLogger(); describe('getAlertsTelemetryData', () => { const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + savedObjectsClient.find.mockResolvedValue({ total: 5, saved_objects: [], @@ -35,7 +38,10 @@ describe('alerts', () => { }); it('it returns the correct res', async () => { - const res = await getAlertsTelemetryData({ savedObjectsClient, logger }); + const res = await getAlertsTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { total: 5, @@ -48,7 +54,7 @@ describe('alerts', () => { }); it('should call find with correct arguments', async () => { - await getAlertsTelemetryData({ savedObjectsClient, logger }); + await getAlertsTelemetryData({ savedObjectsClient: telemetrySavedObjectsClient, logger }); expect(savedObjectsClient.find).toBeCalledWith({ aggs: { counts: { @@ -117,6 +123,7 @@ describe('alerts', () => { page: 0, perPage: 0, type: 'cases-comments', + namespaces: ['*'], }); }); }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/case_system_action.test.ts b/x-pack/plugins/cases/server/telemetry/queries/case_system_action.test.ts index 6009d646431ed..0f121639e0f32 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/case_system_action.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/case_system_action.test.ts @@ -7,12 +7,14 @@ import { loggingSystemMock, savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; import { getCasesSystemActionData } from './case_system_action'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('casesSystemAction', () => { const logger = loggingSystemMock.createLogger(); describe('getCasesSystemActionData', () => { const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); beforeEach(() => { jest.clearAllMocks(); @@ -26,7 +28,10 @@ describe('casesSystemAction', () => { }); it('calculates the metrics correctly', async () => { - const res = await getCasesSystemActionData({ savedObjectsClient, logger }); + const res = await getCasesSystemActionData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ totalCasesCreated: 4, totalRules: 2 }); }); @@ -38,8 +43,49 @@ describe('casesSystemAction', () => { page: 1, }); - const res = await getCasesSystemActionData({ savedObjectsClient, logger }); + const res = await getCasesSystemActionData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); + expect(res).toEqual({ totalCasesCreated: 0, totalRules: 0 }); }); + + it('should call find with correct arguments', async () => { + savedObjectsClient.find.mockResolvedValue({ + total: 1, + saved_objects: [], + per_page: 1, + page: 1, + }); + + await getCasesSystemActionData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); + + expect(savedObjectsClient.find.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "aggs": Object { + "counterSum": Object { + "sum": Object { + "field": "cases-rules.attributes.counter", + }, + }, + "totalRules": Object { + "cardinality": Object { + "field": "cases-rules.attributes.rules.id", + }, + }, + }, + "namespaces": Array [ + "*", + ], + "page": 1, + "perPage": 1, + "type": "cases-rules", + } + `); + }); }); }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/case_system_action.ts b/x-pack/plugins/cases/server/telemetry/queries/case_system_action.ts index 0e05006e3c437..6eda6b477611c 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/case_system_action.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/case_system_action.ts @@ -26,6 +26,7 @@ export const getCasesSystemActionData = async ({ cardinality: { field: `${CASE_RULES_SAVED_OBJECT}.attributes.rules.id` }, }, }, + namespaces: ['*'], }); return { diff --git a/x-pack/plugins/cases/server/telemetry/queries/cases.test.ts b/x-pack/plugins/cases/server/telemetry/queries/cases.test.ts index 560997e8802be..fdfe39f940e9b 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/cases.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/cases.test.ts @@ -15,6 +15,7 @@ import type { FileAttachmentAggregationResults, } from '../types'; import { getCasesTelemetryData } from './cases'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; const MOCK_FIND_TOTAL = 5; const SOLUTION_TOTAL = 1; @@ -23,6 +24,7 @@ describe('getCasesTelemetryData', () => { describe('getCasesTelemetryData', () => { const logger = loggingSystemMock.createLogger(); const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); const mockFind = (aggs: object, so: SavedObjectsFindResponse['saved_objects'] = []) => { savedObjectsClient.find.mockResolvedValueOnce({ @@ -322,7 +324,10 @@ describe('getCasesTelemetryData', () => { }; }; - const res = await getCasesTelemetryData({ savedObjectsClient, logger }); + const res = await getCasesTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); const allAttachmentsTotal = 5; const allAttachmentsAverage = allAttachmentsTotal / MOCK_FIND_TOTAL; @@ -406,7 +411,7 @@ describe('getCasesTelemetryData', () => { it('should call find with correct arguments', async () => { mockResponse(); - await getCasesTelemetryData({ savedObjectsClient, logger }); + await getCasesTelemetryData({ savedObjectsClient: telemetrySavedObjectsClient, logger }); expect(savedObjectsClient.find.mock.calls[0][0]).toMatchInlineSnapshot(` Object { @@ -660,6 +665,9 @@ describe('getCasesTelemetryData', () => { }, }, }, + "namespaces": Array [ + "*", + ], "page": 0, "perPage": 0, "type": "cases", @@ -974,6 +982,9 @@ describe('getCasesTelemetryData', () => { }, }, }, + "namespaces": Array [ + "*", + ], "page": 0, "perPage": 0, "type": "cases-comments", @@ -1023,6 +1034,7 @@ describe('getCasesTelemetryData', () => { page: 0, perPage: 0, type: 'cases-comments', + namespaces: ['*'], }); expect(savedObjectsClient.find.mock.calls[3][0]).toEqual({ @@ -1068,6 +1080,7 @@ describe('getCasesTelemetryData', () => { page: 0, perPage: 0, type: 'cases-user-actions', + namespaces: ['*'], }); for (const [index, sortField] of ['created_at', 'updated_at', 'closed_at'].entries()) { @@ -1079,6 +1092,7 @@ describe('getCasesTelemetryData', () => { sortField, sortOrder: 'desc', type: 'cases', + namespaces: ['*'], }); } @@ -1172,6 +1186,9 @@ describe('getCasesTelemetryData', () => { "function": "is", "type": "function", }, + "namespaces": Array [ + "*", + ], "page": 0, "perPage": 0, "type": "file", diff --git a/x-pack/plugins/cases/server/telemetry/queries/cases.ts b/x-pack/plugins/cases/server/telemetry/queries/cases.ts index abd1979d752e8..81eefd6af1d1d 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/cases.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/cases.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ISavedObjectsRepository, SavedObjectsFindResponse } from '@kbn/core/server'; +import type { SavedObjectsFindResponse } from '@kbn/core/server'; import { FILE_SO_TYPE } from '@kbn/files-plugin/common'; import { fromKueryExpression } from '@kbn/es-query'; import { @@ -37,6 +37,7 @@ import { } from './utils'; import type { CasePersistedAttributes } from '../../common/types/case'; import { CasePersistedStatus } from '../../common/types/case'; +import type { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; export const getLatestCasesDates = async ({ savedObjectsClient, @@ -48,6 +49,7 @@ export const getLatestCasesDates = async ({ sortField, sortOrder: 'desc', type: CASE_SAVED_OBJECT, + namespaces: ['*'], }); const savedObjects = await Promise.all([ @@ -145,7 +147,7 @@ export const getCasesTelemetryData = async ({ }; const getCasesSavedObjectTelemetry = async ( - savedObjectsClient: ISavedObjectsRepository + savedObjectsClient: TelemetrySavedObjectsClient ): Promise> => { const caseByOwnerAggregationQuery = OWNERS.reduce( (aggQuery, owner) => ({ @@ -169,6 +171,7 @@ const getCasesSavedObjectTelemetry = async ( page: 0, perPage: 0, type: CASE_SAVED_OBJECT, + namespaces: ['*'], aggs: { ...caseByOwnerAggregationQuery, ...getCountsAggregationQuery(CASE_SAVED_OBJECT), @@ -231,7 +234,7 @@ const getAssigneesAggregations = () => ({ }); const getCommentsSavedObjectTelemetry = async ( - savedObjectsClient: ISavedObjectsRepository + savedObjectsClient: TelemetrySavedObjectsClient ): Promise> => { const attachmentRegistries = () => ({ externalReferenceTypes: { @@ -275,6 +278,7 @@ const getCommentsSavedObjectTelemetry = async ( page: 0, perPage: 0, type: CASE_COMMENT_SAVED_OBJECT, + namespaces: ['*'], aggs: { ...attachmentsByOwnerAggregationQuery, ...attachmentRegistries(), @@ -288,7 +292,7 @@ const getCommentsSavedObjectTelemetry = async ( }; const getFilesTelemetry = async ( - savedObjectsClient: ISavedObjectsRepository + savedObjectsClient: TelemetrySavedObjectsClient ): Promise> => { const averageSize = () => ({ averageSize: { @@ -332,17 +336,19 @@ const getFilesTelemetry = async ( perPage: 0, type: FILE_SO_TYPE, filter: filterCaseIdExists, + namespaces: ['*'], aggs: { ...filesByOwnerAggregationQuery, ...averageSize(), ...top20MimeTypes() }, }); }; const getAlertsTelemetry = async ( - savedObjectsClient: ISavedObjectsRepository + savedObjectsClient: TelemetrySavedObjectsClient ): Promise> => { return savedObjectsClient.find({ page: 0, perPage: 0, type: CASE_COMMENT_SAVED_OBJECT, + namespaces: ['*'], filter: getOnlyAlertsCommentsFilter(), aggs: { ...getReferencesAggregationQuery({ @@ -355,12 +361,13 @@ const getAlertsTelemetry = async ( }; const getConnectorsTelemetry = async ( - savedObjectsClient: ISavedObjectsRepository + savedObjectsClient: TelemetrySavedObjectsClient ): Promise> => { return savedObjectsClient.find({ page: 0, perPage: 0, type: CASE_USER_ACTION_SAVED_OBJECT, + namespaces: ['*'], filter: getOnlyConnectorsFilter(), aggs: { ...getReferencesAggregationQuery({ diff --git a/x-pack/plugins/cases/server/telemetry/queries/comments.test.ts b/x-pack/plugins/cases/server/telemetry/queries/comments.test.ts index 9eed9b4040992..d3104bd9a79ad 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/comments.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/comments.test.ts @@ -7,11 +7,14 @@ import { loggingSystemMock, savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; import { getUserCommentsTelemetryData } from './comments'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('comments', () => { describe('getUserCommentsTelemetryData', () => { const logger = loggingSystemMock.createLogger(); const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + savedObjectsClient.find.mockResolvedValue({ total: 5, saved_objects: [], @@ -34,7 +37,10 @@ describe('comments', () => { }); it('it returns the correct res', async () => { - const res = await getUserCommentsTelemetryData({ savedObjectsClient, logger }); + const res = await getUserCommentsTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { total: 5, @@ -47,7 +53,10 @@ describe('comments', () => { }); it('should call find with correct arguments', async () => { - await getUserCommentsTelemetryData({ savedObjectsClient, logger }); + await getUserCommentsTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(savedObjectsClient.find).toBeCalledWith({ aggs: { counts: { @@ -116,6 +125,7 @@ describe('comments', () => { page: 0, perPage: 0, type: 'cases-comments', + namespaces: ['*'], }); }); }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/configuration.test.ts b/x-pack/plugins/cases/server/telemetry/queries/configuration.test.ts index 57c7c067a13cf..7e69c60980db1 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/configuration.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/configuration.test.ts @@ -8,11 +8,14 @@ import { loggingSystemMock, savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; import { CustomFieldTypes } from '../../../common/types/domain'; import { getConfigurationTelemetryData } from './configuration'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('configuration', () => { describe('getConfigurationTelemetryData', () => { const logger = loggingSystemMock.createLogger(); const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + savedObjectsClient.find.mockResolvedValue({ total: 5, saved_objects: [], @@ -66,7 +69,10 @@ describe('configuration', () => { }); it('it returns the correct res', async () => { - const res = await getConfigurationTelemetryData({ savedObjectsClient, logger }); + const res = await getConfigurationTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { closure: { @@ -82,7 +88,10 @@ describe('configuration', () => { }); it('should call find with correct arguments', async () => { - await getConfigurationTelemetryData({ savedObjectsClient, logger }); + await getConfigurationTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(savedObjectsClient.find).toBeCalledWith({ aggs: { closureType: { @@ -95,6 +104,7 @@ describe('configuration', () => { page: 1, perPage: 5, type: 'cases-configure', + namespaces: ['*'], }); }); @@ -135,7 +145,10 @@ describe('configuration', () => { }, }); - const res = await getConfigurationTelemetryData({ savedObjectsClient, logger }); + const res = await getConfigurationTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { closure: { @@ -205,7 +218,10 @@ describe('configuration', () => { }, }); - const res = await getConfigurationTelemetryData({ savedObjectsClient, logger }); + const res = await getConfigurationTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { closure: { diff --git a/x-pack/plugins/cases/server/telemetry/queries/configuration.ts b/x-pack/plugins/cases/server/telemetry/queries/configuration.ts index e3aff3216f5d5..6b736761207c8 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/configuration.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/configuration.ts @@ -28,6 +28,7 @@ export const getConfigurationTelemetryData = async ({ page: 1, perPage: 5, type: CASE_CONFIGURE_SAVED_OBJECT, + namespaces: ['*'], aggs: { closureType: { terms: { field: `${CASE_CONFIGURE_SAVED_OBJECT}.attributes.closure_type` }, diff --git a/x-pack/plugins/cases/server/telemetry/queries/connectors.test.ts b/x-pack/plugins/cases/server/telemetry/queries/connectors.test.ts index 684c77bac159a..03779f8714d8d 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/connectors.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/connectors.test.ts @@ -7,11 +7,13 @@ import { savedObjectsRepositoryMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { getConnectorsTelemetryData } from './connectors'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('getConnectorsTelemetryData', () => { describe('getConnectorsTelemetryData', () => { const logger = loggingSystemMock.createLogger(); const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); const mockFind = (aggs: Record) => { savedObjectsClient.find.mockResolvedValueOnce({ @@ -42,7 +44,10 @@ describe('getConnectorsTelemetryData', () => { it('it returns the correct res', async () => { mockResponse(); - const res = await getConnectorsTelemetryData({ savedObjectsClient, logger }); + const res = await getConnectorsTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { all: { @@ -71,7 +76,7 @@ describe('getConnectorsTelemetryData', () => { it('should call find with correct arguments', async () => { mockResponse(); - await getConnectorsTelemetryData({ savedObjectsClient, logger }); + await getConnectorsTelemetryData({ savedObjectsClient: telemetrySavedObjectsClient, logger }); expect(savedObjectsClient.find.mock.calls[0][0]).toEqual({ aggs: { @@ -101,6 +106,7 @@ describe('getConnectorsTelemetryData', () => { page: 0, perPage: 0, type: 'cases-user-actions', + namespaces: ['*'], }); expect(savedObjectsClient.find.mock.calls[1][0]).toEqual({ @@ -151,6 +157,7 @@ describe('getConnectorsTelemetryData', () => { page: 0, perPage: 0, type: 'cases-user-actions', + namespaces: ['*'], }); for (const [index, connector] of [ @@ -205,6 +212,7 @@ describe('getConnectorsTelemetryData', () => { page: 0, perPage: 0, type: 'cases-user-actions', + namespaces: ['*'], }); } }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/connectors.ts b/x-pack/plugins/cases/server/telemetry/queries/connectors.ts index 0e8b12e1ed192..c3f254fadb4ce 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/connectors.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/connectors.ts @@ -37,6 +37,7 @@ export const getConnectorsTelemetryData = async ({ perPage: 0, filter, type: CASE_USER_ACTION_SAVED_OBJECT, + namespaces: ['*'], aggs: { ...aggs, }, diff --git a/x-pack/plugins/cases/server/telemetry/queries/pushed.test.ts b/x-pack/plugins/cases/server/telemetry/queries/push.test.ts similarity index 82% rename from x-pack/plugins/cases/server/telemetry/queries/pushed.test.ts rename to x-pack/plugins/cases/server/telemetry/queries/push.test.ts index e25718f0feac9..1834c5f5d54c0 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/pushed.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/push.test.ts @@ -6,12 +6,15 @@ */ import { savedObjectsRepositoryMock, loggingSystemMock } from '@kbn/core/server/mocks'; -import { getPushedTelemetryData } from './pushes'; +import { getPushedTelemetryData } from './push'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; -describe('pushes', () => { +describe('push', () => { describe('getPushedTelemetryData', () => { const logger = loggingSystemMock.createLogger(); const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + savedObjectsClient.find.mockResolvedValue({ total: 5, saved_objects: [], @@ -27,7 +30,10 @@ describe('pushes', () => { }); it('it returns the correct res', async () => { - const res = await getPushedTelemetryData({ savedObjectsClient, logger }); + const res = await getPushedTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { maxOnACase: 1, @@ -37,7 +43,7 @@ describe('pushes', () => { }); it('should call find with correct arguments', async () => { - await getPushedTelemetryData({ savedObjectsClient, logger }); + await getPushedTelemetryData({ savedObjectsClient: telemetrySavedObjectsClient, logger }); expect(savedObjectsClient.find).toBeCalledWith({ aggs: { references: { @@ -86,6 +92,7 @@ describe('pushes', () => { page: 0, perPage: 0, type: 'cases-user-actions', + namespaces: ['*'], }); }); }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/pushes.ts b/x-pack/plugins/cases/server/telemetry/queries/push.ts similarity index 98% rename from x-pack/plugins/cases/server/telemetry/queries/pushes.ts rename to x-pack/plugins/cases/server/telemetry/queries/push.ts index 0462a7ff0ef13..ea1127ae4520b 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/pushes.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/push.ts @@ -29,6 +29,7 @@ export const getPushedTelemetryData = async ({ perPage: 0, filter: pushFilter, type: CASE_USER_ACTION_SAVED_OBJECT, + namespaces: ['*'], aggs: { ...getMaxBucketOnCaseAggregationQuery(CASE_USER_ACTION_SAVED_OBJECT) }, }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/user_actions.test.ts b/x-pack/plugins/cases/server/telemetry/queries/user_actions.test.ts index c01c8d329c5b0..b6c45d8da3efc 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/user_actions.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/user_actions.test.ts @@ -7,11 +7,14 @@ import { savedObjectsRepositoryMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { getUserActionsTelemetryData } from './user_actions'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('user_actions', () => { describe('getUserActionsTelemetryData', () => { const logger = loggingSystemMock.createLogger(); const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + savedObjectsClient.find.mockResolvedValue({ total: 5, saved_objects: [], @@ -34,7 +37,10 @@ describe('user_actions', () => { }); it('it returns the correct res', async () => { - const res = await getUserActionsTelemetryData({ savedObjectsClient, logger }); + const res = await getUserActionsTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { total: 5, @@ -47,7 +53,10 @@ describe('user_actions', () => { }); it('should call find with correct arguments', async () => { - await getUserActionsTelemetryData({ savedObjectsClient, logger }); + await getUserActionsTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(savedObjectsClient.find).toBeCalledWith({ aggs: { counts: { @@ -101,6 +110,7 @@ describe('user_actions', () => { page: 0, perPage: 0, type: 'cases-user-actions', + namespaces: ['*'], }); }); }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts b/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts index bf975b84f46c5..6c66c5aab81c7 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts @@ -29,6 +29,7 @@ import { getReferencesAggregationQuery, getSolutionValues, } from './utils'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('utils', () => { describe('getSolutionValues', () => { @@ -1017,7 +1018,12 @@ describe('utils', () => { }); it('returns the correct counts and max data', async () => { - const res = await getCountsAndMaxData({ savedObjectsClient, savedObjectType: 'test' }); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + + const res = await getCountsAndMaxData({ + savedObjectsClient: telemetrySavedObjectsClient, + savedObjectType: 'test', + }); expect(res).toEqual({ all: { total: 5, @@ -1030,6 +1036,7 @@ describe('utils', () => { }); it('returns zero data if the response aggregation is not as expected', async () => { + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); savedObjectsClient.find.mockResolvedValue({ total: 5, saved_objects: [], @@ -1037,7 +1044,10 @@ describe('utils', () => { page: 1, }); - const res = await getCountsAndMaxData({ savedObjectsClient, savedObjectType: 'test' }); + const res = await getCountsAndMaxData({ + savedObjectsClient: telemetrySavedObjectsClient, + savedObjectType: 'test', + }); expect(res).toEqual({ all: { total: 5, @@ -1050,7 +1060,13 @@ describe('utils', () => { }); it('should call find with correct arguments', async () => { - await getCountsAndMaxData({ savedObjectsClient, savedObjectType: 'test' }); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + + await getCountsAndMaxData({ + savedObjectsClient: telemetrySavedObjectsClient, + savedObjectType: 'test', + }); + expect(savedObjectsClient.find).toBeCalledWith({ aggs: { counts: { @@ -1104,6 +1120,7 @@ describe('utils', () => { page: 0, perPage: 0, type: 'test', + namespaces: ['*'], }); }); }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/utils.ts b/x-pack/plugins/cases/server/telemetry/queries/utils.ts index ff785077d74ac..65b81e3362300 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/utils.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/utils.ts @@ -7,7 +7,6 @@ import { get } from 'lodash'; import type { KueryNode } from '@kbn/es-query'; -import type { ISavedObjectsRepository } from '@kbn/core/server'; import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, @@ -32,6 +31,7 @@ import type { import { buildFilter } from '../../client/utils'; import type { Owner } from '../../../common/constants/types'; import type { ConfigurationPersistedAttributes } from '../../common/types/configure'; +import type { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; export const getCountsAggregationQuery = (savedObjectType: string) => ({ counts: { @@ -126,7 +126,7 @@ export const getCountsAndMaxData = async ({ savedObjectType, filter, }: { - savedObjectsClient: ISavedObjectsRepository; + savedObjectsClient: TelemetrySavedObjectsClient; savedObjectType: string; filter?: KueryNode; }) => { @@ -138,6 +138,7 @@ export const getCountsAndMaxData = async ({ perPage: 0, filter, type: savedObjectType, + namespaces: ['*'], aggs: { ...getCountsAggregationQuery(savedObjectType), ...getMaxBucketOnCaseAggregationQuery(savedObjectType), diff --git a/x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.test.ts b/x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.test.ts new file mode 100644 index 0000000000000..bbe2d58a1ce9b --- /dev/null +++ b/x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.test.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 { TelemetrySavedObjectsClient } from './telemetry_saved_objects_client'; +import { savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; + +describe('TelemetrySavedObjectsClient', () => { + it("find requests are extended with `namespaces:['*']`", async () => { + const savedObjectsRepository = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsRepository); + + await telemetrySavedObjectsClient.find({ type: 'my-test-type' }); + expect(savedObjectsRepository.find).toBeCalledWith({ type: 'my-test-type', namespaces: ['*'] }); + }); + + it("allow callers to overwrite the `namespaces:['*']`", async () => { + const savedObjectsRepository = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsRepository); + + await telemetrySavedObjectsClient.find({ type: 'my-test-type', namespaces: ['some_space'] }); + expect(savedObjectsRepository.find).toBeCalledWith({ + type: 'my-test-type', + namespaces: ['some_space'], + }); + }); +}); diff --git a/x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.ts b/x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.ts new file mode 100644 index 0000000000000..42ae1fdd296d4 --- /dev/null +++ b/x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsFindOptions, SavedObjectsFindResponse } from '@kbn/core/server'; +import { SavedObjectsClient } from '@kbn/core/server'; + +/** + * Extends the SavedObjectsClient to fit the telemetry fetching requirements (i.e.: find objects from all namespaces by default) + */ +export class TelemetrySavedObjectsClient extends SavedObjectsClient { + /** + * Find the SavedObjects matching the search query in all the Spaces by default + * @param options + */ + async find( + options: SavedObjectsFindOptions + ): Promise> { + return super.find({ namespaces: ['*'], ...options }); + } +} diff --git a/x-pack/plugins/cases/server/telemetry/types.ts b/x-pack/plugins/cases/server/telemetry/types.ts index 294efdbce1125..b4996da27f234 100644 --- a/x-pack/plugins/cases/server/telemetry/types.ts +++ b/x-pack/plugins/cases/server/telemetry/types.ts @@ -5,9 +5,10 @@ * 2.0. */ -import type { ISavedObjectsRepository, Logger } from '@kbn/core/server'; +import type { Logger } from '@kbn/core/server'; import type { MakeSchemaFrom } from '@kbn/usage-collection-plugin/server'; import type { Owner } from '../../common/constants/types'; +import type { TelemetrySavedObjectsClient } from './telemetry_saved_objects_client'; export type BucketKeyString = Omit & { key: string }; @@ -35,7 +36,7 @@ export interface ReferencesAggregation { } export interface CollectTelemetryDataParams { - savedObjectsClient: ISavedObjectsRepository; + savedObjectsClient: TelemetrySavedObjectsClient; logger: Logger; } diff --git a/x-pack/test/cases_api_integration/common/lib/api/index.ts b/x-pack/test/cases_api_integration/common/lib/api/index.ts index cfb0596fa1ce9..ea0f66affdc35 100644 --- a/x-pack/test/cases_api_integration/common/lib/api/index.ts +++ b/x-pack/test/cases_api_integration/common/lib/api/index.ts @@ -61,6 +61,8 @@ export * from './user_profiles'; export * from './omit'; export * from './configuration'; export * from './files'; +export * from './telemetry'; + export { getSpaceUrlPrefix } from './helpers'; function toArray(input: T | T[]): T[] { diff --git a/x-pack/test/cases_api_integration/common/lib/api/telemetry.ts b/x-pack/test/cases_api_integration/common/lib/api/telemetry.ts new file mode 100644 index 0000000000000..785c059249030 --- /dev/null +++ b/x-pack/test/cases_api_integration/common/lib/api/telemetry.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 type SuperTest from 'supertest'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; +import { CasesTelemetry } from '@kbn/cases-plugin/server/telemetry/types'; +import { CASES_TELEMETRY_TASK_NAME } from '@kbn/cases-plugin/common/constants'; + +interface CasesTelemetryPayload { + stats: { stack_stats: { kibana: { plugins: { cases: CasesTelemetry } } } }; +} + +export const getTelemetry = async (supertest: SuperTest.Agent): Promise => { + const { body } = await supertest + .post('/internal/telemetry/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send({ unencrypted: true, refreshCache: true }) + .expect(200); + + return body[0]; +}; + +export const runTelemetryTask = async (supertest: SuperTest.Agent) => { + await supertest + .post('/api/cases_fixture/telemetry/run_soon') + .set('kbn-xsrf', 'xxx') + .send({ taskId: CASES_TELEMETRY_TASK_NAME }) + .expect(200); +}; diff --git a/x-pack/test/cases_api_integration/common/plugins/cases/kibana.jsonc b/x-pack/test/cases_api_integration/common/plugins/cases/kibana.jsonc index 135db481efeef..91238eae39223 100644 --- a/x-pack/test/cases_api_integration/common/plugins/cases/kibana.jsonc +++ b/x-pack/test/cases_api_integration/common/plugins/cases/kibana.jsonc @@ -11,6 +11,7 @@ "features", "cases", "files", + "taskManager" ], "optionalPlugins": [ "security", diff --git a/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts b/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts index a609d014b1c5f..399a9b3d3fdcc 100644 --- a/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts +++ b/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts @@ -12,6 +12,7 @@ import { SecurityPluginStart } from '@kbn/security-plugin/server'; import type { CasesServerStart, CasesServerSetup } from '@kbn/cases-plugin/server'; import { FilesSetup } from '@kbn/files-plugin/server'; import { PluginStartContract as ActionsPluginsStart } from '@kbn/actions-plugin/server/plugin'; +import { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; import { getPersistableStateAttachment } from './attachments/persistable_state'; import { getExternalReferenceAttachment } from './attachments/external_reference'; import { registerRoutes } from './routes'; @@ -28,6 +29,7 @@ export interface FixtureStartDeps { security?: SecurityPluginStart; spaces?: SpacesPluginStart; cases: CasesServerStart; + taskManager: TaskManagerStartContract; } export class FixturePlugin implements Plugin { diff --git a/x-pack/test/cases_api_integration/common/plugins/cases/server/routes.ts b/x-pack/test/cases_api_integration/common/plugins/cases/server/routes.ts index 11335c4d7adc7..10139f636c809 100644 --- a/x-pack/test/cases_api_integration/common/plugins/cases/server/routes.ts +++ b/x-pack/test/cases_api_integration/common/plugins/cases/server/routes.ts @@ -15,6 +15,7 @@ import type { } from '@kbn/cases-plugin/server/attachment_framework/types'; import { BulkCreateCasesRequest, CasesPatchRequest } from '@kbn/cases-plugin/common/types/api'; import { ActionExecutionSourceType } from '@kbn/actions-plugin/server/types'; +import { CASES_TELEMETRY_TASK_NAME } from '@kbn/cases-plugin/common/constants'; import type { FixtureStartDeps } from './plugin'; const hashParts = (parts: string[]): string => { @@ -178,4 +179,32 @@ export const registerRoutes = (core: CoreSetup, logger: Logger } } ); + + router.post( + { + path: '/api/cases_fixture/telemetry/run_soon', + validate: { + body: schema.object({ + taskId: schema.string({ + validate: (telemetryTaskId: string) => { + if (CASES_TELEMETRY_TASK_NAME === telemetryTaskId) { + return; + } + + return 'invalid telemetry task id'; + }, + }), + }), + }, + }, + async (context, req, res) => { + const { taskId } = req.body; + try { + const [_, { taskManager }] = await core.getStartServices(); + return res.ok({ body: await taskManager.runSoon(taskId) }); + } catch (err) { + return res.ok({ body: { id: taskId, error: `${err}` } }); + } + } + ); }; diff --git a/x-pack/test/cases_api_integration/common/plugins/cases/tsconfig.json b/x-pack/test/cases_api_integration/common/plugins/cases/tsconfig.json index 0e0443d2930e9..72a20bd3f40d4 100644 --- a/x-pack/test/cases_api_integration/common/plugins/cases/tsconfig.json +++ b/x-pack/test/cases_api_integration/common/plugins/cases/tsconfig.json @@ -18,6 +18,7 @@ "@kbn/features-plugin", "@kbn/spaces-plugin", "@kbn/security-plugin", + "@kbn/task-manager-plugin", ], "exclude": [ "target/**/*", diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts index e731e0101bdc0..f9360e473080d 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts @@ -70,6 +70,11 @@ export default ({ loadTestFile }: FtrProviderContext): void => { */ loadTestFile(require.resolve('./cases/bulk_create_cases')); + /** + * Telemetry + */ + loadTestFile(require.resolve('./telemetry')); + // NOTE: Migrations are not included because they can inadvertently remove the .kibana indices which removes the users and spaces // which causes errors in any tests after them that relies on those }); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/telemetry.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/telemetry.ts new file mode 100644 index 0000000000000..0c47e62fae79c --- /dev/null +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/telemetry.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { getPostCaseRequest } from '../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + getTelemetry, + runTelemetryTask, +} from '../../../common/lib/api'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { superUser } from '../../../common/lib/authentication/users'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const retry = getService('retry'); + + describe('Cases telemetry', () => { + before(async () => { + await deleteAllCaseItems(es); + }); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should count cases from all spaces', async () => { + await createCase(supertest, getPostCaseRequest(), 200, { + user: superUser, + space: 'space1', + }); + + await createCase(supertest, getPostCaseRequest(), 200, { + user: superUser, + space: 'space2', + }); + + await runTelemetryTask(supertest); + + await retry.try(async () => { + const res = await getTelemetry(supertest); + expect(res.stats.stack_stats.kibana.plugins.cases.cases.all.total).toBe(2); + }); + }); + }); +}; From 607d71decc1091f5bb250c0f8ce729a8e5069adb Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 20 Sep 2024 01:17:26 +1000 Subject: [PATCH 10/19] [8.x] [Security Solution][Detection Engine] log ES requests when running rule preview (#191107) (#193425) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [[Security Solution][Detection Engine] log ES requests when running rule preview (#191107)](https://github.com/elastic/kibana/pull/191107) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com> --- .../rule_preview/rule_preview.gen.ts | 18 ++ .../rule_preview/rule_preview.schema.yaml | 23 ++ .../common/api/quickstart_client.gen.ts | 3 + .../common/experimental_features.ts | 5 + ...ections_api_2023_10_31.bundled.schema.yaml | 24 ++ ...ections_api_2023_10_31.bundled.schema.yaml | 24 ++ .../rule_preview/__mocks__/preview_logs.ts | 93 ++++++ .../components/rule_preview/index.test.tsx | 68 ++++- .../components/rule_preview/index.tsx | 48 ++- .../rule_preview/logged_requests.test.tsx | 68 +++++ .../rule_preview/logged_requests.tsx | 58 ++++ .../rule_preview/logged_requests_item.tsx | 79 +++++ .../rule_preview/optimized_accordion.test.tsx | 59 ++++ .../rule_preview/optimized_accordion.tsx | 39 +++ .../components/rule_preview/preview_logs.tsx | 20 +- .../components/rule_preview/translations.ts | 21 ++ .../rule_preview/use_accordion_styling.ts | 16 + .../rule_preview/use_preview_route.tsx | 3 + .../rule_preview/use_preview_rule.ts | 5 +- .../rule_management/api/api.test.ts | 15 + .../rule_management/api/api.ts | 2 + .../rule_management/logic/types.ts | 6 +- .../rule_preview/api/preview_rules/route.ts | 23 +- .../create_security_rule_type_wrapper.ts | 6 +- .../rule_types/eql/create_eql_alert_type.ts | 5 +- .../rule_types/eql/eql.test.ts | 8 +- .../detection_engine/rule_types/eql/eql.ts | 33 ++- .../detection_engine/rule_types/esql/esql.ts | 279 ++++++++++-------- .../rule_types/esql/fetch_source_documents.ts | 31 +- .../rule_types/translations.ts | 29 ++ .../lib/detection_engine/rule_types/types.ts | 9 +- .../rule_types/utils/logged_requests/index.ts | 10 + .../utils/logged_requests/log_eql.ts | 19 ++ .../utils/logged_requests/log_esql.ts | 15 + .../utils/logged_requests/log_query.ts | 35 +++ .../services/security_solution_api.gen.ts | 9 +- .../config/ess/config.base.ts | 1 + .../configs/serverless.config.ts | 5 +- .../execution_logic/eql.ts | 27 ++ .../execution_logic/esql.ts | 58 ++++ .../utils/rules/preview_rule.ts | 3 + .../test/security_solution_cypress/config.ts | 5 +- .../detection_engine/rule_edit/preview.cy.ts | 85 ++++++ .../cypress/screens/create_new_rule.ts | 16 + .../cypress/tasks/create_new_rule.ts | 20 ++ .../serverless_config.ts | 5 +- 46 files changed, 1268 insertions(+), 165 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/__mocks__/preview_logs.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests_item.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_accordion_styling.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/translations.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/index.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_eql.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_esql.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_query.ts create mode 100644 x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/preview.cy.ts diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.gen.ts index 0e7fb75c2c4c2..ad9b6d9ea12c2 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.gen.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.gen.ts @@ -15,6 +15,7 @@ */ import { z } from '@kbn/zod'; +import { BooleanFromString } from '@kbn/zod-helpers'; import { EqlRuleCreateProps, @@ -34,6 +35,13 @@ export const RulePreviewParams = z.object({ timeframeEnd: z.string().datetime(), }); +export type RulePreviewLoggedRequest = z.infer; +export const RulePreviewLoggedRequest = z.object({ + request: NonEmptyString, + description: NonEmptyString.optional(), + duration: z.number().int().optional(), +}); + export type RulePreviewLogs = z.infer; export const RulePreviewLogs = z.object({ errors: z.array(NonEmptyString), @@ -43,7 +51,17 @@ export const RulePreviewLogs = z.object({ */ duration: z.number().int(), startedAt: NonEmptyString.optional(), + requests: z.array(RulePreviewLoggedRequest).optional(), +}); + +export type RulePreviewRequestQuery = z.infer; +export const RulePreviewRequestQuery = z.object({ + /** + * Enables logging and returning in response ES queries, performed during rule execution + */ + enable_logged_requests: BooleanFromString.optional(), }); +export type RulePreviewRequestQueryInput = z.input; export type RulePreviewRequestBody = z.infer; export const RulePreviewRequestBody = z.discriminatedUnion('type', [ diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.schema.yaml index 1d65d6b4e037e..400b84e533a02 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.schema.yaml @@ -11,6 +11,13 @@ paths: summary: Preview rule alerts generated on specified time range tags: - Rule preview API + parameters: + - name: enable_logged_requests + in: query + description: Enables logging and returning in response ES queries, performed during rule execution + required: false + schema: + type: boolean requestBody: description: An object containing tags to add or remove and alert ids the changes will be applied required: true @@ -94,6 +101,18 @@ components: format: date-time required: [invocationCount, timeframeEnd] + RulePreviewLoggedRequest: + type: object + properties: + request: + $ref: '../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' + description: + $ref: '../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' + duration: + type: integer + required: + - request + RulePreviewLogs: type: object properties: @@ -110,6 +129,10 @@ components: description: Execution duration in milliseconds startedAt: $ref: '../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' + requests: + type: array + items: + $ref: '#/components/schemas/RulePreviewLoggedRequest' required: - errors - warnings diff --git a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts index edd0bfe89fc8c..0249b57883c30 100644 --- a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -99,6 +99,7 @@ import type { GetRuleExecutionResultsResponse, } from './detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.gen'; import type { + RulePreviewRequestQueryInput, RulePreviewRequestBodyInput, RulePreviewResponse, } from './detection_engine/rule_preview/rule_preview.gen'; @@ -1614,6 +1615,7 @@ detection engine rules. }, method: 'POST', body: props.body, + query: props.query, }) .catch(catchAxiosErrorFormatAndThrow); } @@ -1970,6 +1972,7 @@ export interface ResolveTimelineProps { query: ResolveTimelineRequestQueryInput; } export interface RulePreviewProps { + query: RulePreviewRequestQueryInput; body: RulePreviewRequestBodyInput; } export interface SearchAlertsProps { diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 15ad47d5e6c5c..8e206b86a6010 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -133,6 +133,11 @@ export const allowedExperimentalValues = Object.freeze({ */ esqlRulesDisabled: false, + /** + * enables logging requests during rule preview + */ + loggingRequestsEnabled: false, + /** * Enables Protection Updates tab in the Endpoint Policy Details page */ diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml index 8642113778fe0..9642a2129ba60 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml @@ -891,6 +891,15 @@ paths: /api/detection_engine/rules/preview: post: operationId: RulePreview + parameters: + - description: >- + Enables logging and returning in response ES queries, performed + during rule execution + in: query + name: enable_logged_requests + required: false + schema: + type: boolean requestBody: content: application/json: @@ -5178,6 +5187,17 @@ components: - $ref: '#/components/schemas/MachineLearningRulePatchProps' - $ref: '#/components/schemas/NewTermsRulePatchProps' - $ref: '#/components/schemas/EsqlRulePatchProps' + RulePreviewLoggedRequest: + type: object + properties: + description: + $ref: '#/components/schemas/NonEmptyString' + duration: + type: integer + request: + $ref: '#/components/schemas/NonEmptyString' + required: + - request RulePreviewLogs: type: object properties: @@ -5188,6 +5208,10 @@ components: items: $ref: '#/components/schemas/NonEmptyString' type: array + requests: + items: + $ref: '#/components/schemas/RulePreviewLoggedRequest' + type: array startedAt: $ref: '#/components/schemas/NonEmptyString' warnings: diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml index 514c4c87405cd..410d27479e19f 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml @@ -476,6 +476,15 @@ paths: /api/detection_engine/rules/preview: post: operationId: RulePreview + parameters: + - description: >- + Enables logging and returning in response ES queries, performed + during rule execution + in: query + name: enable_logged_requests + required: false + schema: + type: boolean requestBody: content: application/json: @@ -4331,6 +4340,17 @@ components: - $ref: '#/components/schemas/MachineLearningRulePatchProps' - $ref: '#/components/schemas/NewTermsRulePatchProps' - $ref: '#/components/schemas/EsqlRulePatchProps' + RulePreviewLoggedRequest: + type: object + properties: + description: + $ref: '#/components/schemas/NonEmptyString' + duration: + type: integer + request: + $ref: '#/components/schemas/NonEmptyString' + required: + - request RulePreviewLogs: type: object properties: @@ -4341,6 +4361,10 @@ components: items: $ref: '#/components/schemas/NonEmptyString' type: array + requests: + items: + $ref: '#/components/schemas/RulePreviewLoggedRequest' + type: array startedAt: $ref: '#/components/schemas/NonEmptyString' warnings: diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/__mocks__/preview_logs.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/__mocks__/preview_logs.ts new file mode 100644 index 0000000000000..1e380d1bb4561 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/__mocks__/preview_logs.ts @@ -0,0 +1,93 @@ +/* + * 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 { RulePreviewLogs } from '../../../../../../common/api/detection_engine'; + +export const previewLogs: RulePreviewLogs[] = [ + { + errors: [], + warnings: [], + startedAt: '2024-09-05T15:43:46.972Z', + duration: 149, + requests: [ + { + request: + 'POST _query\n{\n "query": "FROM packetbeat-8.14.2 metadata _id, _version, _index | limit 101",\n "filter": {\n "bool": {\n "filter": [\n {\n "range": {\n "@timestamp": {\n "lte": "2024-09-05T15:43:46.972Z",\n "gte": "2024-09-05T15:22:46.972Z",\n "format": "strict_date_optional_time"\n }\n }\n },\n {\n "bool": {\n "must": [],\n "filter": [],\n "should": [],\n "must_not": []\n }\n }\n ]\n }\n }\n}', + description: 'ES|QL request to find all matches', + duration: 23, + }, + { + request: + 'POST /packetbeat-8.14.2/_search?ignore_unavailable=true\n{\n "query": {\n "bool": {\n "filter": {\n "ids": {\n "values": [\n "yB7awpEBluhaSO8ejVKZ",\n "yR7awpEBluhaSO8ejVKZ",\n "yh7awpEBluhaSO8ejVKZ",\n "yx7awpEBluhaSO8ejVKZ",\n "zB7awpEBluhaSO8ejVKZ",\n "zR7awpEBluhaSO8ejVKZ",\n "zh7awpEBluhaSO8ejVKZ",\n "zx7awpEBluhaSO8ejVKZ",\n "0B7awpEBluhaSO8ejVKZ",\n "0R7awpEBluhaSO8ejVKZ",\n "0h7awpEBluhaSO8ejVKZ",\n "0x7awpEBluhaSO8ejVKZ",\n "1B7awpEBluhaSO8ejVKZ",\n "1R7awpEBluhaSO8ejVKZ",\n "1h7awpEBluhaSO8ejVKZ",\n "1x7awpEBluhaSO8ejVKZ",\n "2B7awpEBluhaSO8ejVKZ",\n "2R7awpEBluhaSO8ejVKZ",\n "2h7awpEBluhaSO8ejVKZ",\n "2x7awpEBluhaSO8ejVKZ",\n "3B7awpEBluhaSO8ejVKZ",\n "3R7awpEBluhaSO8ejVKZ",\n "3h7awpEBluhaSO8ejVKZ",\n "3x7awpEBluhaSO8ejVKZ",\n "4B7awpEBluhaSO8ejVKZ",\n "4R7awpEBluhaSO8ejVKZ",\n "4h7awpEBluhaSO8ejVKZ",\n "4x7awpEBluhaSO8ejVKZ",\n "5B7awpEBluhaSO8ejVKZ",\n "5R7awpEBluhaSO8ejVKZ",\n "5h7awpEBluhaSO8ejVKZ",\n "5x7awpEBluhaSO8ejVKZ",\n "6B7awpEBluhaSO8ejVKZ",\n "6R7awpEBluhaSO8ejVKZ",\n "6h7awpEBluhaSO8ejVKZ",\n "6x7awpEBluhaSO8ejVKZ",\n "7B7awpEBluhaSO8ejVKZ",\n "7R7awpEBluhaSO8ejVKZ",\n "7h7awpEBluhaSO8ejVKZ",\n "7x7awpEBluhaSO8ejVKZ",\n "8B7awpEBluhaSO8ejVKZ",\n "8R7awpEBluhaSO8ejVKZ",\n "8h7awpEBluhaSO8ejVKZ",\n "8x7awpEBluhaSO8ejVKZ",\n "9B7awpEBluhaSO8ejVKZ",\n "9R7awpEBluhaSO8ejVKZ",\n "9h7awpEBluhaSO8ejVKZ",\n "9x7awpEBluhaSO8ejVKZ",\n "-B7awpEBluhaSO8ejVKZ",\n "-R7awpEBluhaSO8ejVKZ",\n "-h7awpEBluhaSO8ejVKZ",\n "-x7awpEBluhaSO8ejVKZ",\n "_B7awpEBluhaSO8ejVKZ",\n "_R7awpEBluhaSO8ejVKZ",\n "_h7awpEBluhaSO8ejVKZ",\n "_x7awpEBluhaSO8ejVKZ",\n "AB7awpEBluhaSO8ejVOZ",\n "AR7awpEBluhaSO8ejVOZ",\n "Ah7awpEBluhaSO8ejVOZ",\n "Ax7awpEBluhaSO8ejVOZ",\n "BB7awpEBluhaSO8ejVOZ",\n "BR7awpEBluhaSO8ejVOZ",\n "Bh7awpEBluhaSO8ejVOZ",\n "Bx7awpEBluhaSO8ejVOZ",\n "CB7awpEBluhaSO8ejVOZ",\n "CR7awpEBluhaSO8ejVOZ",\n "Ch7awpEBluhaSO8ejVOZ",\n "Cx7awpEBluhaSO8ejVOZ",\n "DB7awpEBluhaSO8ejVOZ",\n "DR7awpEBluhaSO8ejVOZ",\n "Dh7awpEBluhaSO8ejVOZ",\n "Dx7awpEBluhaSO8ejVOZ",\n "EB7awpEBluhaSO8ejVOZ",\n "ER7awpEBluhaSO8ejVOZ",\n "Eh7awpEBluhaSO8ejVOZ",\n "Ex7awpEBluhaSO8ejVOZ",\n "FB7awpEBluhaSO8ejVOZ",\n "FR7awpEBluhaSO8ejVOZ",\n "Fh7awpEBluhaSO8ejVOZ",\n "Fx7awpEBluhaSO8ejVOZ",\n "GB7awpEBluhaSO8ejVOZ",\n "GR7awpEBluhaSO8ejVOZ",\n "Gh7awpEBluhaSO8ejVOZ",\n "Gx7awpEBluhaSO8ejVOZ",\n "HB7awpEBluhaSO8ejVOZ",\n "HR7awpEBluhaSO8ejVOZ",\n "Hh7awpEBluhaSO8ejVOZ",\n "Hx7awpEBluhaSO8ejVOZ",\n "IB7awpEBluhaSO8ejVOZ",\n "IR7awpEBluhaSO8ejVOZ",\n "Ih7awpEBluhaSO8ejVOZ",\n "Ix7awpEBluhaSO8ejVOZ",\n "JB7awpEBluhaSO8ejVOZ",\n "JR7awpEBluhaSO8ejVOZ",\n "Jh7awpEBluhaSO8ejVOZ",\n "Jx7awpEBluhaSO8ejVOZ",\n "KB7awpEBluhaSO8ejVOZ",\n "KR7awpEBluhaSO8ejVOZ",\n "Kh7awpEBluhaSO8ejVOZ",\n "Kx7awpEBluhaSO8ejVOZ",\n "LB7awpEBluhaSO8ejVOZ"\n ]\n }\n }\n }\n },\n "_source": false,\n "fields": [\n "*"\n ]\n}', + description: 'Retrieve source documents when ES|QL query is not aggregable', + duration: 8, + }, + ], + }, + { + errors: [], + warnings: [], + startedAt: '2024-09-05T16:03:46.972Z', + duration: 269, + requests: [ + { + request: + 'POST _query\n{\n "query": "FROM packetbeat-8.14.2 metadata _id, _version, _index | limit 101",\n "filter": {\n "bool": {\n "filter": [\n {\n "range": {\n "@timestamp": {\n "lte": "2024-09-05T16:03:46.972Z",\n "gte": "2024-09-05T15:42:46.972Z",\n "format": "strict_date_optional_time"\n }\n }\n },\n {\n "bool": {\n "must": [],\n "filter": [],\n "should": [],\n "must_not": []\n }\n }\n ]\n }\n }\n}', + description: 'ES|QL request to find all matches', + duration: 30, + }, + { + request: + 'POST /packetbeat-8.14.2/_search?ignore_unavailable=true\n{\n "query": {\n "bool": {\n "filter": {\n "ids": {\n "values": [\n "yB7awpEBluhaSO8ejVKZ",\n "yR7awpEBluhaSO8ejVKZ",\n "yh7awpEBluhaSO8ejVKZ",\n "yx7awpEBluhaSO8ejVKZ",\n "zB7awpEBluhaSO8ejVKZ",\n "zR7awpEBluhaSO8ejVKZ",\n "zh7awpEBluhaSO8ejVKZ",\n "zx7awpEBluhaSO8ejVKZ",\n "0B7awpEBluhaSO8ejVKZ",\n "0R7awpEBluhaSO8ejVKZ",\n "0h7awpEBluhaSO8ejVKZ",\n "0x7awpEBluhaSO8ejVKZ",\n "1B7awpEBluhaSO8ejVKZ",\n "1R7awpEBluhaSO8ejVKZ",\n "1h7awpEBluhaSO8ejVKZ",\n "1x7awpEBluhaSO8ejVKZ",\n "2B7awpEBluhaSO8ejVKZ",\n "2R7awpEBluhaSO8ejVKZ",\n "2h7awpEBluhaSO8ejVKZ",\n "2x7awpEBluhaSO8ejVKZ",\n "3B7awpEBluhaSO8ejVKZ",\n "3R7awpEBluhaSO8ejVKZ",\n "3h7awpEBluhaSO8ejVKZ",\n "3x7awpEBluhaSO8ejVKZ",\n "4B7awpEBluhaSO8ejVKZ",\n "4R7awpEBluhaSO8ejVKZ",\n "4h7awpEBluhaSO8ejVKZ",\n "4x7awpEBluhaSO8ejVKZ",\n "5B7awpEBluhaSO8ejVKZ",\n "5R7awpEBluhaSO8ejVKZ",\n "5h7awpEBluhaSO8ejVKZ",\n "5x7awpEBluhaSO8ejVKZ",\n "6B7awpEBluhaSO8ejVKZ",\n "6R7awpEBluhaSO8ejVKZ",\n "6h7awpEBluhaSO8ejVKZ",\n "6x7awpEBluhaSO8ejVKZ",\n "7B7awpEBluhaSO8ejVKZ",\n "7R7awpEBluhaSO8ejVKZ",\n "7h7awpEBluhaSO8ejVKZ",\n "7x7awpEBluhaSO8ejVKZ",\n "8B7awpEBluhaSO8ejVKZ",\n "8R7awpEBluhaSO8ejVKZ",\n "8h7awpEBluhaSO8ejVKZ",\n "8x7awpEBluhaSO8ejVKZ",\n "9B7awpEBluhaSO8ejVKZ",\n "9R7awpEBluhaSO8ejVKZ",\n "9h7awpEBluhaSO8ejVKZ",\n "9x7awpEBluhaSO8ejVKZ",\n "-B7awpEBluhaSO8ejVKZ",\n "-R7awpEBluhaSO8ejVKZ",\n "-h7awpEBluhaSO8ejVKZ",\n "-x7awpEBluhaSO8ejVKZ",\n "_B7awpEBluhaSO8ejVKZ",\n "_R7awpEBluhaSO8ejVKZ",\n "_h7awpEBluhaSO8ejVKZ",\n "_x7awpEBluhaSO8ejVKZ",\n "AB7awpEBluhaSO8ejVOZ",\n "AR7awpEBluhaSO8ejVOZ",\n "Ah7awpEBluhaSO8ejVOZ",\n "Ax7awpEBluhaSO8ejVOZ",\n "BB7awpEBluhaSO8ejVOZ",\n "BR7awpEBluhaSO8ejVOZ",\n "Bh7awpEBluhaSO8ejVOZ",\n "Bx7awpEBluhaSO8ejVOZ",\n "CB7awpEBluhaSO8ejVOZ",\n "CR7awpEBluhaSO8ejVOZ",\n "Ch7awpEBluhaSO8ejVOZ",\n "Cx7awpEBluhaSO8ejVOZ",\n "DB7awpEBluhaSO8ejVOZ",\n "DR7awpEBluhaSO8ejVOZ",\n "Dh7awpEBluhaSO8ejVOZ",\n "Dx7awpEBluhaSO8ejVOZ",\n "EB7awpEBluhaSO8ejVOZ",\n "ER7awpEBluhaSO8ejVOZ",\n "Eh7awpEBluhaSO8ejVOZ",\n "Ex7awpEBluhaSO8ejVOZ",\n "FB7awpEBluhaSO8ejVOZ",\n "FR7awpEBluhaSO8ejVOZ",\n "Fh7awpEBluhaSO8ejVOZ",\n "Fx7awpEBluhaSO8ejVOZ",\n "GB7awpEBluhaSO8ejVOZ",\n "GR7awpEBluhaSO8ejVOZ",\n "Gh7awpEBluhaSO8ejVOZ",\n "Gx7awpEBluhaSO8ejVOZ",\n "HB7awpEBluhaSO8ejVOZ",\n "HR7awpEBluhaSO8ejVOZ",\n "Hh7awpEBluhaSO8ejVOZ",\n "Hx7awpEBluhaSO8ejVOZ",\n "IB7awpEBluhaSO8ejVOZ",\n "IR7awpEBluhaSO8ejVOZ",\n "Ih7awpEBluhaSO8ejVOZ",\n "Ix7awpEBluhaSO8ejVOZ",\n "JB7awpEBluhaSO8ejVOZ",\n "JR7awpEBluhaSO8ejVOZ",\n "Jh7awpEBluhaSO8ejVOZ",\n "Jx7awpEBluhaSO8ejVOZ",\n "KB7awpEBluhaSO8ejVOZ",\n "KR7awpEBluhaSO8ejVOZ",\n "Kh7awpEBluhaSO8ejVOZ",\n "Kx7awpEBluhaSO8ejVOZ",\n "LB7awpEBluhaSO8ejVOZ"\n ]\n }\n }\n }\n },\n "_source": false,\n "fields": [\n "*"\n ]\n}', + description: 'Retrieve source documents when ES|QL query is not aggregable', + duration: 6, + }, + { + request: + 'POST _query\n{\n "query": "FROM packetbeat-8.14.2 metadata _id, _version, _index | limit 201",\n "filter": {\n "bool": {\n "filter": [\n {\n "range": {\n "@timestamp": {\n "lte": "2024-09-05T16:03:46.972Z",\n "gte": "2024-09-05T15:42:46.972Z",\n "format": "strict_date_optional_time"\n }\n }\n },\n {\n "bool": {\n "must": [],\n "filter": [],\n "should": [],\n "must_not": []\n }\n }\n ]\n }\n }\n}', + description: 'ES|QL request to find all matches', + }, + { + request: + 'POST /packetbeat-8.14.2/_search?ignore_unavailable=true\n{\n "query": {\n "bool": {\n "filter": {\n "ids": {\n "values": [\n "LB7awpEBluhaSO8ejVOZ",\n "LR7awpEBluhaSO8ejVOZ",\n "Lh7awpEBluhaSO8ejVOZ",\n "Lx7awpEBluhaSO8ejVOZ",\n "MB7awpEBluhaSO8ejVOZ",\n "MR7awpEBluhaSO8ejVOZ",\n "Mh7awpEBluhaSO8ejVOZ",\n "Mx7awpEBluhaSO8ejVOZ",\n "NB7awpEBluhaSO8ejVOZ",\n "NR7awpEBluhaSO8ejVOZ",\n "Nh7awpEBluhaSO8ejVOZ",\n "Nx7awpEBluhaSO8ejVOZ",\n "OB7awpEBluhaSO8ejVOZ",\n "OR7awpEBluhaSO8ejVOZ",\n "Oh7awpEBluhaSO8ejVOZ",\n "Ox7awpEBluhaSO8ejVOZ",\n "PB7awpEBluhaSO8ejVOZ",\n "PR7awpEBluhaSO8ejVOZ",\n "Ph7awpEBluhaSO8ejVOZ",\n "Px7awpEBluhaSO8ejVOZ",\n "QB7awpEBluhaSO8ejVOZ",\n "QR7awpEBluhaSO8ejVOZ",\n "Qh7awpEBluhaSO8ejVOZ",\n "Qx7awpEBluhaSO8ejVOZ",\n "RB7awpEBluhaSO8ejVOZ",\n "RR7awpEBluhaSO8ejVOZ",\n "Rh7awpEBluhaSO8ejVOZ",\n "Rx7awpEBluhaSO8ejVOZ",\n "SB7awpEBluhaSO8ejVOZ",\n "SR7awpEBluhaSO8ejVOZ",\n "Sx7awpEBluhaSO8ewFOg",\n "TB7awpEBluhaSO8ewFOg",\n "TR7awpEBluhaSO8ewFOg",\n "Th7awpEBluhaSO8ewFOg",\n "Tx7awpEBluhaSO8ewFOg",\n "UB7awpEBluhaSO8ewFOg",\n "UR7awpEBluhaSO8ewFOg",\n "Uh7awpEBluhaSO8ewFOh",\n "Ux7awpEBluhaSO8ewFOh",\n "VB7awpEBluhaSO8ewFOh",\n "VR7awpEBluhaSO8ewFOh",\n "Vh7awpEBluhaSO8ewFOh",\n "Vx7awpEBluhaSO8ewFOh",\n "WB7awpEBluhaSO8ewFOh",\n "WR7awpEBluhaSO8ewFOh",\n "Wh7awpEBluhaSO8ewFOh",\n "Wx7awpEBluhaSO8ewFOh",\n "XB7awpEBluhaSO8ewFOh",\n "XR7awpEBluhaSO8ewFOh",\n "Xh7awpEBluhaSO8ewFOh",\n "Xx7awpEBluhaSO8ewFOh",\n "YB7awpEBluhaSO8ewFOh",\n "YR7awpEBluhaSO8ewFOh",\n "Yh7awpEBluhaSO8ewFOh",\n "Yx7awpEBluhaSO8ewFOh",\n "ZB7awpEBluhaSO8ewFOh",\n "ZR7awpEBluhaSO8ewFOh",\n "Zh7awpEBluhaSO8ewFOh",\n "Zx7awpEBluhaSO8ewFOh",\n "aB7awpEBluhaSO8ewFOh",\n "aR7awpEBluhaSO8ewFOh",\n "ah7awpEBluhaSO8ewFOh",\n "ax7awpEBluhaSO8ewFOh",\n "bB7awpEBluhaSO8ewFOh",\n "bR7awpEBluhaSO8ewFOh",\n "bh7awpEBluhaSO8ewFOh",\n "bx7awpEBluhaSO8ewFOh",\n "cB7awpEBluhaSO8ewFOh",\n "cR7awpEBluhaSO8ewFOh",\n "ch7awpEBluhaSO8ewFOh",\n "cx7awpEBluhaSO8ewFOh",\n "dB7awpEBluhaSO8ewFOh",\n "dR7awpEBluhaSO8ewFOh",\n "dh7awpEBluhaSO8ewFOh",\n "dx7awpEBluhaSO8ewFOh",\n "eB7awpEBluhaSO8ewFOh",\n "eR7awpEBluhaSO8ewFOh",\n "eh7awpEBluhaSO8ewFOh",\n "ex7awpEBluhaSO8ewFOh",\n "fB7awpEBluhaSO8ewFOh",\n "fR7awpEBluhaSO8ewFOh",\n "fh7awpEBluhaSO8ewFOh",\n "fx7awpEBluhaSO8ewFOh",\n "gB7awpEBluhaSO8ewFOh",\n "gR7awpEBluhaSO8ewFOh",\n "gh7awpEBluhaSO8ewFOh",\n "gx7awpEBluhaSO8ewFOh",\n "hB7awpEBluhaSO8ewFOh",\n "hR7awpEBluhaSO8ewFOh",\n "hh7awpEBluhaSO8ewFOh",\n "hx7awpEBluhaSO8ewFOh",\n "iB7awpEBluhaSO8ewFOh",\n "iR7awpEBluhaSO8ewFOh",\n "ih7awpEBluhaSO8ewFOh",\n "ix7awpEBluhaSO8ewFOh",\n "jB7awpEBluhaSO8ewFOh",\n "jR7awpEBluhaSO8ewFOh",\n "jh7awpEBluhaSO8ewFOh",\n "jx7awpEBluhaSO8ewFOh",\n "kB7awpEBluhaSO8ewFOh",\n "kR7awpEBluhaSO8ewFOh"\n ]\n }\n }\n }\n },\n "_source": false,\n "fields": [\n "*"\n ]\n}', + description: 'Retrieve source documents when ES|QL query is not aggregable', + duration: 8, + }, + { + request: + 'POST _query\n{\n "query": "FROM packetbeat-8.14.2 metadata _id, _version, _index | limit 301",\n "filter": {\n "bool": {\n "filter": [\n {\n "range": {\n "@timestamp": {\n "lte": "2024-09-05T16:03:46.972Z",\n "gte": "2024-09-05T15:42:46.972Z",\n "format": "strict_date_optional_time"\n }\n }\n },\n {\n "bool": {\n "must": [],\n "filter": [],\n "should": [],\n "must_not": []\n }\n }\n ]\n }\n }\n}', + description: 'ES|QL request to find all matches', + }, + { + request: + 'POST /packetbeat-8.14.2/_search?ignore_unavailable=true\n{\n "query": {\n "bool": {\n "filter": {\n "ids": {\n "values": [\n "kR7awpEBluhaSO8ewFOh",\n "kh7awpEBluhaSO8ewFOh",\n "kx7awpEBluhaSO8ewFOh",\n "lB7awpEBluhaSO8ewFOh",\n "lR7awpEBluhaSO8ewFOh",\n "lh7awpEBluhaSO8ewFOh",\n "lx7awpEBluhaSO8ewFOh",\n "mB7awpEBluhaSO8ewFOh",\n "mR7awpEBluhaSO8ewFOh",\n "mh7awpEBluhaSO8ewFOh",\n "mx7awpEBluhaSO8ewFOh",\n "nB7awpEBluhaSO8ewFOh",\n "nR7awpEBluhaSO8ewFOh",\n "nh7awpEBluhaSO8ewFOh",\n "nx7awpEBluhaSO8ewFOh",\n "oB7awpEBluhaSO8ewFOh",\n "oR7awpEBluhaSO8ewFOh",\n "oh7awpEBluhaSO8ewFOh",\n "ox7awpEBluhaSO8ewFOh",\n "pB7awpEBluhaSO8ewFOh",\n "pR7awpEBluhaSO8ewFOh",\n "ph7awpEBluhaSO8ewFOh",\n "px7awpEBluhaSO8ewFOh",\n "qB7awpEBluhaSO8ewFOh",\n "qR7awpEBluhaSO8ewFOh",\n "qh7awpEBluhaSO8ewFOh",\n "qx7awpEBluhaSO8ewFOh",\n "rB7awpEBluhaSO8ewFOh",\n "rR7awpEBluhaSO8ewFOh",\n "rh7awpEBluhaSO8ewFOh",\n "rx7awpEBluhaSO8ewFOh",\n "sB7awpEBluhaSO8ewFOh",\n "sR7awpEBluhaSO8ewFOh",\n "sh7awpEBluhaSO8ewFOh",\n "sx7awpEBluhaSO8ewFOh",\n "tB7awpEBluhaSO8ewFOh",\n "tR7awpEBluhaSO8ewFOh",\n "th7awpEBluhaSO8ewFOh",\n "tx7awpEBluhaSO8ewFOh",\n "uB7awpEBluhaSO8ewFOh",\n "uR7awpEBluhaSO8ewFOh",\n "uh7awpEBluhaSO8ewFOh",\n "ux7awpEBluhaSO8ewFOh",\n "vB7awpEBluhaSO8ewFOh",\n "vR7awpEBluhaSO8ewFOh",\n "vh7awpEBluhaSO8ewFOh",\n "vx7awpEBluhaSO8ewFOh",\n "wB7awpEBluhaSO8ewFOh",\n "wR7awpEBluhaSO8ewFOh",\n "wh7awpEBluhaSO8ewFOh",\n "wx7awpEBluhaSO8ewFOh",\n "xB7awpEBluhaSO8ewFOh",\n "xR7awpEBluhaSO8ewFOh",\n "xh7awpEBluhaSO8ewFOh",\n "xx7awpEBluhaSO8ewFOh",\n "yB7awpEBluhaSO8ewFOh",\n "yR7awpEBluhaSO8ewFOh",\n "yh7awpEBluhaSO8ewFOh",\n "yx7awpEBluhaSO8ewFOh",\n "zB7awpEBluhaSO8ewFOh",\n "zR7awpEBluhaSO8ewFOh",\n "zh7awpEBluhaSO8ewFOh",\n "zx7awpEBluhaSO8ewFOh",\n "0B7awpEBluhaSO8ewFOh",\n "0R7awpEBluhaSO8ewFOh",\n "0h7awpEBluhaSO8ewFOh",\n "0x7awpEBluhaSO8ewFOh",\n "1B7awpEBluhaSO8ewFOh",\n "1R7awpEBluhaSO8ewFOh",\n "1h7awpEBluhaSO8ewFOh",\n "1x7awpEBluhaSO8ewFOh",\n "2B7awpEBluhaSO8ewFOh",\n "2R7awpEBluhaSO8ewFOh",\n "2h7awpEBluhaSO8ewFOh",\n "2x7awpEBluhaSO8ewFOh",\n "3B7awpEBluhaSO8ewFOh",\n "3R7awpEBluhaSO8ewFOh",\n "3h7awpEBluhaSO8ewFOh",\n "3x7awpEBluhaSO8ewFOh",\n "4B7awpEBluhaSO8ewFOh",\n "4R7awpEBluhaSO8ewFOh",\n "4h7awpEBluhaSO8ewFOh",\n "4x7awpEBluhaSO8ewFOh",\n "5B7awpEBluhaSO8ewFOh",\n "5R7awpEBluhaSO8ewFOh",\n "5h7awpEBluhaSO8ewFOh",\n "6h7awpEBluhaSO8e51Pb",\n "6x7awpEBluhaSO8e51Pb",\n "7B7awpEBluhaSO8e51Pb",\n "7R7awpEBluhaSO8e51Pb",\n "7h7awpEBluhaSO8e51Pb",\n "7x7awpEBluhaSO8e51Pb",\n "8B7awpEBluhaSO8e51Pb",\n "8R7awpEBluhaSO8e51Pb",\n "8h7awpEBluhaSO8e51Pb",\n "8x7awpEBluhaSO8e51Pb",\n "9B7awpEBluhaSO8e51Pb",\n "9R7awpEBluhaSO8e51Pb",\n "9h7awpEBluhaSO8e51Pb",\n "9x7awpEBluhaSO8e51Pb",\n "-B7awpEBluhaSO8e51Pb"\n ]\n }\n }\n }\n },\n "_source": false,\n "fields": [\n "*"\n ]\n}', + description: 'Retrieve source documents when ES|QL query is not aggregable', + duration: 7, + }, + ], + }, + { + errors: [], + warnings: [], + startedAt: '2024-09-05T16:23:46.972Z', + duration: 103, + requests: [ + { + request: + 'POST _query\n{\n "query": "FROM packetbeat-8.14.2 metadata _id, _version, _index | limit 101",\n "filter": {\n "bool": {\n "filter": [\n {\n "range": {\n "@timestamp": {\n "lte": "2024-09-05T16:23:46.972Z",\n "gte": "2024-09-05T16:02:46.972Z",\n "format": "strict_date_optional_time"\n }\n }\n },\n {\n "bool": {\n "must": [],\n "filter": [],\n "should": [],\n "must_not": []\n }\n }\n ]\n }\n }\n}', + description: 'ES|QL request to find all matches', + duration: 19, + }, + { + request: + 'POST /packetbeat-8.14.2/_search?ignore_unavailable=true\n{\n "query": {\n "bool": {\n "filter": {\n "ids": {\n "values": [\n "_B7_wpEBluhaSO8enqFT",\n "_R7_wpEBluhaSO8enqFT",\n "_h7_wpEBluhaSO8enqFT",\n "_x7_wpEBluhaSO8enqFT",\n "AB7_wpEBluhaSO8enqJT",\n "AR7_wpEBluhaSO8enqJT",\n "Ah7_wpEBluhaSO8enqJT",\n "Ax7_wpEBluhaSO8enqJT",\n "BB7_wpEBluhaSO8enqJT",\n "BR7_wpEBluhaSO8enqJT",\n "Bh7_wpEBluhaSO8enqJT",\n "Bx7_wpEBluhaSO8enqJT",\n "CB7_wpEBluhaSO8enqJT",\n "CR7_wpEBluhaSO8enqJT",\n "Ch7_wpEBluhaSO8enqJT",\n "Cx7_wpEBluhaSO8enqJT",\n "DB7_wpEBluhaSO8enqJT",\n "DR7_wpEBluhaSO8enqJT",\n "Dh7_wpEBluhaSO8enqJT",\n "Dx7_wpEBluhaSO8enqJT",\n "EB7_wpEBluhaSO8enqJT",\n "ER7_wpEBluhaSO8enqJT",\n "Eh7_wpEBluhaSO8enqJT",\n "Ex7_wpEBluhaSO8enqJT",\n "FB7_wpEBluhaSO8enqJT",\n "FR7_wpEBluhaSO8enqJT",\n "Fh7_wpEBluhaSO8enqJT",\n "Fx7_wpEBluhaSO8enqJT",\n "GB7_wpEBluhaSO8enqJT",\n "GR7_wpEBluhaSO8enqJT",\n "Gh7_wpEBluhaSO8enqJT",\n "Gx7_wpEBluhaSO8enqJT",\n "tR7wwpEBluhaSO8efnLO",\n "th7wwpEBluhaSO8efnLO",\n "tx7wwpEBluhaSO8efnLO",\n "uB7wwpEBluhaSO8efnLO",\n "uR7wwpEBluhaSO8efnLO",\n "uh7wwpEBluhaSO8efnLO",\n "ux7wwpEBluhaSO8efnLO",\n "vB7wwpEBluhaSO8efnLO",\n "vR7wwpEBluhaSO8efnLO",\n "vh7wwpEBluhaSO8efnLO",\n "vx7wwpEBluhaSO8efnLO",\n "wB7wwpEBluhaSO8efnLO",\n "wR7wwpEBluhaSO8efnLO",\n "wh7wwpEBluhaSO8efnLO",\n "wx7wwpEBluhaSO8efnLO",\n "xB7wwpEBluhaSO8efnLO",\n "xR7wwpEBluhaSO8efnLO",\n "xh7wwpEBluhaSO8efnLO",\n "xx7wwpEBluhaSO8efnLO",\n "yB7wwpEBluhaSO8efnLO",\n "yR7wwpEBluhaSO8efnLO",\n "yh7wwpEBluhaSO8efnLO",\n "yx7wwpEBluhaSO8efnLO",\n "zB7wwpEBluhaSO8efnLO",\n "zR7wwpEBluhaSO8efnLO",\n "zh7wwpEBluhaSO8efnLO",\n "zx7wwpEBluhaSO8efnLO",\n "0B7wwpEBluhaSO8efnLO",\n "0R7wwpEBluhaSO8efnLO",\n "0h7wwpEBluhaSO8efnLO",\n "0x7wwpEBluhaSO8efnLO",\n "1B7wwpEBluhaSO8efnLO",\n "1B7twpEBluhaSO8eu1-P",\n "1R7twpEBluhaSO8eu1-P",\n "1h7twpEBluhaSO8eu1-P",\n "1x7twpEBluhaSO8eu1-P",\n "2B7twpEBluhaSO8eu1-P",\n "2R7twpEBluhaSO8eu1-P",\n "2h7twpEBluhaSO8eu1-P",\n "2x7twpEBluhaSO8eu1-P",\n "3B7twpEBluhaSO8eu1-P",\n "3R7twpEBluhaSO8eu1-P",\n "3h7twpEBluhaSO8eu1-P",\n "3x7twpEBluhaSO8eu1-P",\n "4B7twpEBluhaSO8eu1-P",\n "4R7twpEBluhaSO8eu1-P",\n "4h7twpEBluhaSO8eu1-P",\n "4x7twpEBluhaSO8eu1-P",\n "5B7twpEBluhaSO8eu1-P",\n "5R7twpEBluhaSO8eu1-P",\n "5h7twpEBluhaSO8eu1-P",\n "5x7twpEBluhaSO8eu1-P",\n "6B7twpEBluhaSO8eu1-P",\n "6R7twpEBluhaSO8eu1-P",\n "6h7twpEBluhaSO8eu1-P",\n "6x7twpEBluhaSO8eu1-P",\n "7B7twpEBluhaSO8eu1-P",\n "7R7twpEBluhaSO8eu1-P",\n "7h7twpEBluhaSO8eu1-P",\n "7x7twpEBluhaSO8eu1-P",\n "8B7twpEBluhaSO8eu1-P",\n "8R7twpEBluhaSO8eu1-P",\n "8h7twpEBluhaSO8eu1-P",\n "8x7twpEBluhaSO8eu1-P",\n "HB7_wpEBluhaSO8enqJT",\n "HR7_wpEBluhaSO8enqJT",\n "Hh7_wpEBluhaSO8enqJT",\n "Hx7_wpEBluhaSO8enqJT",\n "IB7_wpEBluhaSO8enqJT"\n ]\n }\n }\n }\n },\n "_source": false,\n "fields": [\n "*"\n ]\n}', + description: 'Retrieve source documents when ES|QL query is not aggregable', + duration: 5, + }, + ], + }, +]; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.test.tsx index 69eebec3452d5..4ebb460177476 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.test.tsx @@ -6,10 +6,11 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import type { DataViewBase } from '@kbn/es-query'; import { fields } from '@kbn/data-plugin/common/mocks'; +import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { TestProviders } from '../../../../common/mock'; import type { RulePreviewProps } from '.'; @@ -22,6 +23,7 @@ import { stepDefineDefaultValue, } from '../../../../detections/pages/detection_engine/rules/utils'; import { usePreviewInvocationCount } from './use_preview_invocation_count'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; jest.mock('../../../../common/lib/kibana'); jest.mock('./use_preview_route'); @@ -34,6 +36,21 @@ jest.mock('../../../../common/containers/use_global_time', () => ({ }), })); jest.mock('./use_preview_invocation_count'); +jest.mock('../../../../common/hooks/use_experimental_features', () => ({ + useIsExperimentalFeatureEnabled: jest.fn(), +})); + +const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; +// rule types that do not support logged requests +const doNotSupportLoggedRequests: Type[] = [ + 'threshold', + 'threat_match', + 'machine_learning', + 'query', + 'new_terms', +]; + +const supportLoggedRequests: Type[] = ['esql', 'eql']; const getMockIndexPattern = (): DataViewBase => ({ fields, @@ -97,6 +114,8 @@ describe('PreviewQuery', () => { }); (usePreviewInvocationCount as jest.Mock).mockReturnValue({ invocationCount: 500 }); + + useIsExperimentalFeatureEnabledMock.mockReturnValue(true); }); afterEach(() => { @@ -137,4 +156,51 @@ describe('PreviewQuery', () => { expect(await wrapper.findByTestId('previewInvocationCountWarning')).toBeTruthy(); }); + + supportLoggedRequests.forEach((ruleType) => { + test(`renders "Show Elasticsearch requests" for ${ruleType} rule type`, () => { + render( + + + + ); + + expect(screen.getByTestId('show-elasticsearch-requests')).toBeInTheDocument(); + }); + }); + + supportLoggedRequests.forEach((ruleType) => { + test(`does not render "Show Elasticsearch requests" for ${ruleType} rule type when feature is disabled`, () => { + useIsExperimentalFeatureEnabledMock.mockReturnValue(false); + + render( + + + + ); + + expect(screen.queryByTestId('show-elasticsearch-requests')).toBeNull(); + }); + }); + + doNotSupportLoggedRequests.forEach((ruleType) => { + test(`does not render "Show Elasticsearch requests" for ${ruleType} rule type`, () => { + render( + + + + ); + + expect(screen.queryByTestId('show-elasticsearch-requests')).toBeNull(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.tsx index 8deb2ebd41863..2a86600d94e7a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.tsx @@ -18,9 +18,12 @@ import { EuiText, EuiTitle, EuiFormRow, + EuiCheckbox, } from '@elastic/eui'; import moment from 'moment'; import type { List } from '@kbn/securitysolution-io-ts-list-types'; +import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; + import { isEqual } from 'lodash'; import * as i18n from './translations'; import { usePreviewRoute } from './use_preview_route'; @@ -37,9 +40,12 @@ import type { TimeframePreviewOptions, } from '../../../../detections/pages/detection_engine/rules/types'; import { usePreviewInvocationCount } from './use_preview_invocation_count'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; export const REASONABLE_INVOCATION_COUNT = 200; +const RULE_TYPES_SUPPORTING_LOGGED_REQUESTS: Type[] = ['esql', 'eql']; + const timeRanges = [ { start: 'now/d', end: 'now', label: 'Today' }, { start: 'now/w', end: 'now', label: 'This week' }, @@ -64,6 +70,7 @@ interface RulePreviewState { aboutRuleData?: AboutStepRule; scheduleRuleData?: ScheduleStepRule; timeframeOptions: TimeframePreviewOptions; + enableLoggedRequests?: boolean; } const refreshedTimeframe = (startDate: string, endDate: string) => { @@ -83,6 +90,8 @@ const RulePreviewComponent: React.FC = ({ const { indexPattern, ruleType } = defineRuleData; const { spaces } = useKibana().services; + const isLoggingRequestsFeatureEnabled = useIsExperimentalFeatureEnabled('loggingRequestsEnabled'); + const [spaceId, setSpaceId] = useState(''); useEffect(() => { if (spaces) { @@ -98,6 +107,8 @@ const RulePreviewComponent: React.FC = ({ const [timeframeStart, setTimeframeStart] = useState(moment().subtract(1, 'hour')); const [timeframeEnd, setTimeframeEnd] = useState(moment()); + const [showElasticsearchRequests, setShowElasticsearchRequests] = useState(false); + const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false); useEffect(() => { @@ -140,6 +151,7 @@ const RulePreviewComponent: React.FC = ({ scheduleRuleData: previewData.scheduleRuleData, exceptionsList, timeframeOptions: previewData.timeframeOptions, + enableLoggedRequests: previewData.enableLoggedRequests, }); const { startTransaction } = useStartTransaction(); @@ -185,9 +197,18 @@ const RulePreviewComponent: React.FC = ({ interval: scheduleRuleData.interval, lookback: scheduleRuleData.from, }, + enableLoggedRequests: showElasticsearchRequests, }); setIsRefreshing(true); - }, [aboutRuleData, defineRuleData, endDate, scheduleRuleData, startDate, startTransaction]); + }, [ + aboutRuleData, + defineRuleData, + endDate, + scheduleRuleData, + startDate, + startTransaction, + showElasticsearchRequests, + ]); const isDirty = useMemo( () => @@ -261,6 +282,24 @@ const RulePreviewComponent: React.FC = ({ + {isLoggingRequestsFeatureEnabled && + RULE_TYPES_SUPPORTING_LOGGED_REQUESTS.includes(ruleType) ? ( + + + + { + setShowElasticsearchRequests(!showElasticsearchRequests); + }} + /> + + + + ) : null} {isPreviewRequestInProgress && } {!isPreviewRequestInProgress && previewId && spaceId && ( @@ -273,7 +312,12 @@ const RulePreviewComponent: React.FC = ({ timeframeOptions={previewData.timeframeOptions} /> )} - + ); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.test.tsx new file mode 100644 index 0000000000000..c0cf8870c162a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.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 React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { TestProviders } from '../../../../common/mock/test_providers'; +import { LoggedRequests } from './logged_requests'; + +import { previewLogs } from './__mocks__/preview_logs'; + +describe('LoggedRequests', () => { + it('should not render component if logs are empty', () => { + render(, { wrapper: TestProviders }); + + expect(screen.queryByTestId('preview-logged-requests-accordion')).toBeNull(); + }); + + it('should open accordion on click and render list of request items', async () => { + render(, { wrapper: TestProviders }); + + expect(screen.queryByTestId('preview-logged-requests-accordion')).toBeInTheDocument(); + + await userEvent.click(screen.getByText('Preview logged requests')); + + expect(screen.getAllByTestId('preview-logged-requests-item-accordion')).toHaveLength(3); + }); + + it('should render code content on logged request item accordion click', async () => { + render(, { wrapper: TestProviders }); + + expect(screen.queryByTestId('preview-logged-requests-accordion')).toBeInTheDocument(); + + await userEvent.click(screen.getByText('Preview logged requests')); + + // picking up second rule execution + const loggedRequestsItem = screen.getAllByTestId('preview-logged-requests-item-accordion')[1]; + + expect(loggedRequestsItem).toHaveTextContent('Rule execution started at'); + expect(loggedRequestsItem).toHaveTextContent('[269ms]'); + + await userEvent.click(loggedRequestsItem.querySelector('button') as HTMLElement); + + expect(screen.getAllByTestId('preview-logged-request-description')).toHaveLength(6); + expect(screen.getAllByTestId('preview-logged-request-code-block')).toHaveLength(6); + + expect(screen.getAllByTestId('preview-logged-request-description')[0]).toHaveTextContent( + 'ES|QL request to find all matches [30ms]' + ); + + expect(screen.getAllByTestId('preview-logged-request-code-block')[0]).toHaveTextContent( + /FROM packetbeat-8\.14\.2 metadata _id, _version, _index \| limit 101/ + ); + + expect(screen.getAllByTestId('preview-logged-request-description')[1]).toHaveTextContent( + 'Retrieve source documents when ES|QL query is not aggregable' + ); + + expect(screen.getAllByTestId('preview-logged-request-code-block')[1]).toHaveTextContent( + /POST \/packetbeat-8\.14\.2\/_search\?ignore_unavailable=true/ + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.tsx new file mode 100644 index 0000000000000..d7b62c6f08c69 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.tsx @@ -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 type { FC } from 'react'; +import React, { useMemo } from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { css } from '@emotion/css'; + +import type { RulePreviewLogs } from '../../../../../common/api/detection_engine'; +import * as i18n from './translations'; +import { OptimizedAccordion } from './optimized_accordion'; +import { LoggedRequestsItem } from './logged_requests_item'; +import { useAccordionStyling } from './use_accordion_styling'; + +const LoggedRequestsComponent: FC<{ logs: RulePreviewLogs[] }> = ({ logs }) => { + const cssStyles = useAccordionStyling(); + + const AccordionContent = useMemo( + () => ( + <> + + {logs.map((log) => ( + + + + ))} + + ), + [logs] + ); + + if (logs.length === 0) { + return null; + } + + return ( + <> + + {AccordionContent} + + + ); +}; + +export const LoggedRequests = React.memo(LoggedRequestsComponent); +LoggedRequests.displayName = 'LoggedRequests'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests_item.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests_item.tsx new file mode 100644 index 0000000000000..2f2e7d74bf826 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests_item.tsx @@ -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 { FC, PropsWithChildren } from 'react'; +import React from 'react'; +import { css } from '@emotion/css'; + +import { EuiSpacer, EuiCodeBlock, useEuiPaddingSize, EuiFlexItem } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { RulePreviewLogs } from '../../../../../common/api/detection_engine'; +import * as i18n from './translations'; +import { PreferenceFormattedDate } from '../../../../common/components/formatted_date'; +import { OptimizedAccordion } from './optimized_accordion'; +import { useAccordionStyling } from './use_accordion_styling'; + +const LoggedRequestsItemComponent: FC> = ({ + startedAt, + duration, + requests, +}) => { + const paddingLarge = useEuiPaddingSize('l'); + const cssStyles = useAccordionStyling(); + + return ( + + {startedAt ? ( + }} + /> + ) : ( + i18n.LOGGED_REQUEST_ITEM_ACCORDION_UNKNOWN_TIME_BUTTON + )} + {`[${duration}ms]`} + + } + id={`ruleExecution-${startedAt}`} + css={css` + margin-left: ${paddingLarge}; + ${cssStyles} + `} + > + {(requests ?? []).map((request, key) => ( + + + + {request?.description ?? null} {request?.duration ? `[${request.duration}ms]` : null} + + + + {request.request} + + + ))} + + ); +}; + +export const LoggedRequestsItem = React.memo(LoggedRequestsItemComponent); +LoggedRequestsItem.displayName = 'LoggedRequestsItem'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.test.tsx new file mode 100644 index 0000000000000..bab5751aa6ddf --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.test.tsx @@ -0,0 +1,59 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { OptimizedAccordion } from './optimized_accordion'; + +describe('OptimizedAccordion', () => { + it('should not render children content if accordion initially closed', () => { + render( + + {'content'} + + ); + + expect(screen.queryByText('content')).toBeNull(); + }); + it('should render children content if accordion initially opened', () => { + render( + + {'content'} + + ); + + expect(screen.getByText('content')).toBeInTheDocument(); + }); + it('should render children content when accordion opened', async () => { + render( + + {'content'} + + ); + + const toggleButton = screen.getByText('accordion button'); + await userEvent.click(toggleButton); + + expect(screen.getByText('content')).toBeVisible(); + }); + it('should not destroy children content when accordion closed', async () => { + render( + + {'content'} + + ); + + const toggleButton = screen.getByText('accordion button'); + await userEvent.click(toggleButton); + + expect(screen.getByText('content')).toBeVisible(); + + await userEvent.click(toggleButton); + expect(screen.getByText('content')).not.toBeVisible(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.tsx new file mode 100644 index 0000000000000..5d3ad0ab874be --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FC } from 'react'; + +import React, { useState } from 'react'; + +import type { EuiAccordionProps } from '@elastic/eui'; +import { EuiAccordion } from '@elastic/eui'; + +/** + * component does not render children before it was opened + * once children rendered for the first time, they won't be re-rendered on subsequent accordion toggling + */ +const OptimizedAccordionComponent: FC = ({ children, ...props }) => { + const [trigger, setTrigger] = useState<'closed' | 'open'>('closed'); + const [isRendered, setIsRendered] = useState(false); + + const onToggle = (isOpen: boolean) => { + const newState = isOpen ? 'open' : 'closed'; + if (isOpen) { + setIsRendered(true); + } + setTrigger(newState); + }; + + return ( + + {isRendered || props.forceState === 'open' ? children : null} + + ); +}; + +export const OptimizedAccordion = React.memo(OptimizedAccordionComponent); +OptimizedAccordion.displayName = 'OptimizedAccordion'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_logs.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_logs.tsx index 912df2453cc46..196408cbc1371 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_logs.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_logs.tsx @@ -7,14 +7,19 @@ import type { FC, PropsWithChildren } from 'react'; import React, { Fragment, useMemo } from 'react'; +import { css } from '@emotion/css'; import { EuiCallOut, EuiText, EuiSpacer, EuiAccordion } from '@elastic/eui'; + import type { RulePreviewLogs } from '../../../../../common/api/detection_engine'; import * as i18n from './translations'; +import { LoggedRequests } from './logged_requests'; +import { useAccordionStyling } from './use_accordion_styling'; interface PreviewLogsProps { logs: RulePreviewLogs[]; hasNoiseWarning: boolean; isAborted: boolean; + showElasticsearchRequests: boolean; } interface SortedLogs { @@ -43,7 +48,12 @@ const addLogs = ( allLogs: SortedLogs[] ) => (logs.length ? [{ startedAt, logs, duration }, ...allLogs] : allLogs); -const PreviewLogsComponent: React.FC = ({ logs, hasNoiseWarning, isAborted }) => { +const PreviewLogsComponent: React.FC = ({ + logs, + hasNoiseWarning, + isAborted, + showElasticsearchRequests, +}) => { const sortedLogs = useMemo( () => logs.reduce<{ @@ -66,6 +76,7 @@ const PreviewLogsComponent: React.FC = ({ logs, hasNoiseWarnin {isAborted ? : null} + {showElasticsearchRequests ? : null} ); }; @@ -74,6 +85,8 @@ export const PreviewLogs = React.memo(PreviewLogsComponent); PreviewLogs.displayName = 'PreviewLogs'; const LogAccordion: FC> = ({ logs, isError, children }) => { + const cssStyles = useAccordionStyling(); + const firstLog = logs[0]; if (!(children || firstLog)) return null; @@ -96,6 +109,10 @@ const LogAccordion: FC> = ({ logs, isError, buttonContent={ isError ? i18n.QUERY_PREVIEW_SEE_ALL_ERRORS : i18n.QUERY_PREVIEW_SEE_ALL_WARNINGS } + borders="horizontal" + css={css` + ${cssStyles} + `} > {restOfLogs.map((log, key) => ( > = ({ logs, isError, ))} ) : null} - ); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/translations.ts index 4ab9328ee806b..0b25071a76830 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/translations.ts @@ -158,6 +158,27 @@ export const VIEW_DETAILS = i18n.translate( } ); +export const ENABLED_LOGGED_REQUESTS_CHECKBOX = i18n.translate( + 'xpack.securitySolution.detectionEngine.queryPreview.enabledLoggedRequestsLabel', + { + defaultMessage: 'Show Elasticsearch requests, ran during rule executions', + } +); + +export const LOGGED_REQUESTS_ACCORDION_BUTTON = i18n.translate( + 'xpack.securitySolution.detectionEngine.queryPreview.loggedRequestsAccordionButtonLabel', + { + defaultMessage: 'Preview logged requests', + } +); + +export const LOGGED_REQUEST_ITEM_ACCORDION_UNKNOWN_TIME_BUTTON = i18n.translate( + 'xpack.securitySolution.detectionEngine.queryPreview.loggedRequestItemAccordionUnknownTimeButtonLabel', + { + defaultMessage: 'Preview logged requests', + } +); + export const VIEW_DETAILS_FOR_ROW = ({ ariaRowindex, columnValues, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_accordion_styling.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_accordion_styling.ts new file mode 100644 index 0000000000000..be05d90836c94 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_accordion_styling.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 { useEuiPaddingSize } from '@elastic/eui'; + +export const useAccordionStyling = () => { + const paddingLarge = useEuiPaddingSize('l'); + const paddingSmall = useEuiPaddingSize('s'); + + return `padding-bottom: ${paddingLarge}; + padding-top: ${paddingSmall};`; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_route.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_route.tsx index 25952956b2538..5684819106ada 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_route.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_route.tsx @@ -24,6 +24,7 @@ interface PreviewRouteParams { scheduleRuleData?: ScheduleStepRule; exceptionsList?: List[]; timeframeOptions: TimeframePreviewOptions; + enableLoggedRequests?: boolean; } export const usePreviewRoute = ({ @@ -32,6 +33,7 @@ export const usePreviewRoute = ({ scheduleRuleData, exceptionsList, timeframeOptions, + enableLoggedRequests, }: PreviewRouteParams) => { const [isRequestTriggered, setIsRequestTriggered] = useState(false); @@ -41,6 +43,7 @@ export const usePreviewRoute = ({ const { isLoading, response, rule, setRule } = usePreviewRule({ timeframeOptions, + enableLoggedRequests, }); const [logs, setLogs] = useState(response.logs ?? []); const [isAborted, setIsAborted] = useState(!!response.isAborted); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts index 9fb7417bca036..05c3b9fe10299 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts @@ -27,8 +27,10 @@ const emptyPreviewRule: RulePreviewResponse = { export const usePreviewRule = ({ timeframeOptions, + enableLoggedRequests, }: { timeframeOptions: TimeframePreviewOptions; + enableLoggedRequests?: boolean; }) => { const [rule, setRule] = useState(null); const [response, setResponse] = useState(emptyPreviewRule); @@ -66,6 +68,7 @@ export const usePreviewRule = ({ invocationCount, timeframeEnd, }, + enableLoggedRequests, signal: abortCtrl.signal, }); if (isSubscribed) { @@ -87,7 +90,7 @@ export const usePreviewRule = ({ isSubscribed = false; abortCtrl.abort(); }; - }, [rule, addError, invocationCount, from, interval, timeframeEnd]); + }, [rule, addError, invocationCount, from, interval, timeframeEnd, enableLoggedRequests]); return { isLoading, response, rule, setRule }; }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts index 5180f9fb891f0..d10bb4bb03e08 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts @@ -117,6 +117,21 @@ describe('Detections Rules API', () => { expect.objectContaining({ body: '{"description":"Detecting root and admin users","name":"Query with a rule id","query":"user.name: root or user.name: admin","severity":"high","type":"query","risk_score":55,"language":"kuery","rule_id":"rule-1","invocationCount":1,"timeframeEnd":"2015-03-12 05:17:10"}', method: 'POST', + query: undefined, + }) + ); + }); + + test('sends enable_logged_requests in URL query', async () => { + const payload = getCreateRulesSchemaMock(); + await previewRule({ + rule: { ...payload, invocationCount: 1, timeframeEnd: '2015-03-12 05:17:10' }, + enableLoggedRequests: true, + }); + expect(fetchMock).toHaveBeenCalledWith( + '/api/detection_engine/rules/preview', + expect.objectContaining({ + query: { enable_logged_requests: true }, }) ); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts index 4254b58234400..c86606d0d8137 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts @@ -150,6 +150,7 @@ export const patchRule = async ({ */ export const previewRule = async ({ rule, + enableLoggedRequests, signal, }: PreviewRulesProps): Promise => KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_PREVIEW, { @@ -157,6 +158,7 @@ export const previewRule = async ({ version: '2023-10-31', body: JSON.stringify(rule), signal, + query: enableLoggedRequests ? { enable_logged_requests: enableLoggedRequests } : undefined, }); /** diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts index 526d0a00389d7..e12442c97aa4c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts @@ -32,7 +32,11 @@ export interface CreateRulesProps { } export interface PreviewRulesProps { - rule: RuleCreateProps & { invocationCount: number; timeframeEnd: string }; + rule: RuleCreateProps & { + invocationCount: number; + timeframeEnd: string; + }; + enableLoggedRequests?: boolean; signal?: AbortSignal; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts index 52f4d3739b1e2..50542592aa1d1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts @@ -31,7 +31,11 @@ import type { RulePreviewResponse, RulePreviewLogs, } from '../../../../../../common/api/detection_engine'; -import { RulePreviewRequestBody } from '../../../../../../common/api/detection_engine'; +import { + RulePreviewRequestBody, + RulePreviewRequestQuery, +} from '../../../../../../common/api/detection_engine'; +import type { RulePreviewLoggedRequest } from '../../../../../../common/api/detection_engine/rule_preview/rule_preview.gen'; import type { StartPlugins, SetupPlugins } from '../../../../../plugin'; import { buildSiemResponse } from '../../../routes/utils'; @@ -92,7 +96,12 @@ export const previewRulesRoute = ( .addVersion( { version: '2023-10-31', - validate: { request: { body: buildRouteValidationWithZod(RulePreviewRequestBody) } }, + validate: { + request: { + body: buildRouteValidationWithZod(RulePreviewRequestBody), + query: buildRouteValidationWithZod(RulePreviewRequestQuery), + }, + }, }, async (context, request, response): Promise> => { const siemResponse = buildSiemResponse(response); @@ -143,7 +152,9 @@ export const previewRulesRoute = ( const username = security?.authc.getCurrentUser(request)?.username; const loggedStatusChanges: Array = []; const previewRuleExecutionLogger = createPreviewRuleExecutionLogger(loggedStatusChanges); - const runState: Record = {}; + const runState: Record = { + isLoggedRequestsEnabled: request.query.enable_logged_requests, + }; const logs: RulePreviewLogs[] = []; let isAborted = false; @@ -224,6 +235,7 @@ export const previewRulesRoute = ( } ) => { let statePreview = runState as TState; + let loggedRequests = []; const abortController = new AbortController(); setTimeout(() => { @@ -268,7 +280,7 @@ export const previewRulesRoute = ( while (invocationCount > 0 && !isAborted) { invocationStartTime = moment(); - ({ state: statePreview } = (await executor({ + ({ state: statePreview, loggedRequests } = (await executor({ executionId: uuidv4(), params, previousStartedAt, @@ -302,7 +314,7 @@ export const previewRulesRoute = ( const date = startedAt.toISOString(); return { dateStart: date, dateEnd: date }; }, - })) as { state: TState }); + })) as { state: TState; loggedRequests: RulePreviewLoggedRequest[] }); const errors = loggedStatusChanges .filter((item) => item.newStatus === RuleExecutionStatusEnum.failed) @@ -317,6 +329,7 @@ export const previewRulesRoute = ( warnings, startedAt: startedAt.toDate().toISOString(), duration: moment().diff(invocationStartTime, 'milliseconds'), + ...(loggedRequests ? { requests: loggedRequests } : {}), }); loggedStatusChanges.length = 0; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index 1e5e70a37ae5f..f25a8429089b6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -467,6 +467,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = warning: warningMessages.length > 0, warningMessages, userError: runResult.userError, + ...(runResult.loggedRequests ? { loggedRequests: runResult.loggedRequests } : {}), }; runState = runResult.state; } @@ -571,7 +572,10 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = }); } - return { state: result.state }; + return { + state: result.state, + ...(result.loggedRequests ? { loggedRequests: result.loggedRequests } : {}), + }; }); }, alerts: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts index ca16b38404e48..9de8641d7b17c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts @@ -111,7 +111,7 @@ export const createEqlAlertType = ( alertSuppression: completeRule.ruleParams.alertSuppression, licensing, }); - const result = await eqlExecutor({ + const { result, loggedRequests } = await eqlExecutor({ completeRule, tuple, inputIndex, @@ -131,9 +131,10 @@ export const createEqlAlertType = ( alertWithSuppression, isAlertSuppressionActive: isNonSeqAlertSuppressionActive, experimentalFeatures, + state, scheduleNotificationResponseActionsService, }); - return { ...result, state }; + return { ...result, state, ...(loggedRequests ? { loggedRequests } : {}) }; }, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.test.ts index 9ef9faeb9de3a..4f5aa7d322c9e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.test.ts @@ -54,7 +54,7 @@ describe('eql_executor', () => { describe('eqlExecutor', () => { describe('warning scenarios', () => { it('warns when exception list for eql rule contains value list exceptions', async () => { - const result = await eqlExecutor({ + const { result } = await eqlExecutor({ inputIndex: DEFAULT_INDEX_PATTERN, runtimeMappings: {}, completeRule: eqlCompleteRule, @@ -105,7 +105,7 @@ describe('eql_executor', () => { }, }); - const result = await eqlExecutor({ + const { result } = await eqlExecutor({ inputIndex: DEFAULT_INDEX_PATTERN, runtimeMappings: {}, completeRule: ruleWithSequenceAndSuppression, @@ -140,7 +140,7 @@ describe('eql_executor', () => { message: 'verification_exception\n\tRoot causes:\n\t\tverification_exception: Found 1 problem\nline 1:1: Unknown column [event.category]', }); - const result = await eqlExecutor({ + const { result } = await eqlExecutor({ inputIndex: DEFAULT_INDEX_PATTERN, runtimeMappings: {}, completeRule: eqlCompleteRule, @@ -165,7 +165,7 @@ describe('eql_executor', () => { }); it('should handle scheduleNotificationResponseActionsService call', async () => { - const result = await eqlExecutor({ + const { result } = await eqlExecutor({ inputIndex: DEFAULT_INDEX_PATTERN, runtimeMappings: {}, completeRule: eqlCompleteRule, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts index 3379d0a0c6867..47e298392d7d9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts @@ -46,6 +46,9 @@ import type { import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring'; import { bulkCreateSuppressedAlertsInMemory } from '../utils/bulk_create_suppressed_alerts_in_memory'; import { getDataTierFilter } from '../utils/get_data_tier_filter'; +import type { RulePreviewLoggedRequest } from '../../../../../common/api/detection_engine/rule_preview/rule_preview.gen'; +import { logEqlRequest } from '../utils/logged_requests'; +import * as i18n from '../translations'; interface EqlExecutorParams { inputIndex: string[]; @@ -67,6 +70,7 @@ interface EqlExecutorParams { alertWithSuppression: SuppressedAlertService; isAlertSuppressionActive: boolean; experimentalFeatures: ExperimentalFeatures; + state?: Record; scheduleNotificationResponseActionsService: CreateRuleAdditionalOptions['scheduleNotificationResponseActionsService']; } @@ -90,10 +94,17 @@ export const eqlExecutor = async ({ alertWithSuppression, isAlertSuppressionActive, experimentalFeatures, + state, scheduleNotificationResponseActionsService, -}: EqlExecutorParams): Promise => { +}: EqlExecutorParams): Promise<{ + result: SearchAfterAndBulkCreateReturnType; + loggedRequests?: RulePreviewLoggedRequest[]; +}> => { const ruleParams = completeRule.ruleParams; + const isLoggedRequestsEnabled = state?.isLoggedRequestsEnabled ?? false; + const loggedRequests: RulePreviewLoggedRequest[] = []; + // eslint-disable-next-line complexity return withSecuritySpan('eqlExecutor', async () => { const result = createSearchAfterReturnType(); @@ -125,13 +136,24 @@ export const eqlExecutor = async ({ const eqlSignalSearchStart = performance.now(); try { + if (isLoggedRequestsEnabled) { + loggedRequests.push({ + request: logEqlRequest(request), + description: i18n.EQL_SEARCH_REQUEST_DESCRIPTION, + }); + } + const response = await services.scopedClusterClient.asCurrentUser.eql.search( request ); const eqlSignalSearchEnd = performance.now(); - const eqlSearchDuration = makeFloatString(eqlSignalSearchEnd - eqlSignalSearchStart); - result.searchAfterTimes = [eqlSearchDuration]; + const eqlSearchDuration = eqlSignalSearchEnd - eqlSignalSearchStart; + result.searchAfterTimes = [makeFloatString(eqlSearchDuration)]; + + if (isLoggedRequestsEnabled && loggedRequests[0]) { + loggedRequests[0].duration = Math.round(eqlSearchDuration); + } let newSignals: Array> | undefined; @@ -198,8 +220,7 @@ export const eqlExecutor = async ({ responseActions: completeRule.ruleParams.responseActions, }); } - - return result; + return { result, ...(isLoggedRequestsEnabled ? { loggedRequests } : {}) }; } catch (error) { if ( typeof error.message === 'string' && @@ -211,7 +232,7 @@ export const eqlExecutor = async ({ } result.errors.push(error.message); result.success = false; - return result; + return { result, ...(isLoggedRequestsEnabled ? { loggedRequests } : {}) }; } }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts index 0dd2b0e50d4ba..1e5b1749e94f5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts @@ -27,8 +27,11 @@ import { createEnrichEventsFunction } from '../utils/enrichments'; import { rowToDocument } from './utils'; import { fetchSourceDocuments } from './fetch_source_documents'; import { buildReasonMessageForEsqlAlert } from '../utils/reason_formatters'; - +import type { RulePreviewLoggedRequest } from '../../../../../common/api/detection_engine/rule_preview/rule_preview.gen'; import type { RunOpts, SignalSource, CreateRuleAdditionalOptions } from '../types'; +import { logEsqlRequest } from '../utils/logged_requests'; +import * as i18n from '../translations'; + import { addToSearchAfterReturn, createSearchAfterReturnType, @@ -66,13 +69,14 @@ export const esqlExecutor = async ({ }: { runOpts: RunOpts; services: RuleExecutorServices; - state: object; + state: Record; spaceId: string; version: string; experimentalFeatures: ExperimentalFeatures; licensing: LicensingPluginSetup; scheduleNotificationResponseActionsService: CreateRuleAdditionalOptions['scheduleNotificationResponseActionsService']; }) => { + const loggedRequests: RulePreviewLoggedRequest[] = []; const ruleParams = completeRule.ruleParams; /** * ES|QL returns results as a single page. max size of 10,000 @@ -80,91 +84,80 @@ export const esqlExecutor = async ({ * we don't want to overload ES/Kibana with large responses */ const ESQL_PAGE_SIZE_CIRCUIT_BREAKER = tuple.maxSignals * 3; + const isLoggedRequestsEnabled = state?.isLoggedRequestsEnabled ?? false; return withSecuritySpan('esqlExecutor', async () => { const result = createSearchAfterReturnType(); let size = tuple.maxSignals; - while ( - result.createdSignalsCount <= tuple.maxSignals && - size <= ESQL_PAGE_SIZE_CIRCUIT_BREAKER - ) { - const esqlRequest = buildEsqlSearchRequest({ - query: ruleParams.query, - from: tuple.from.toISOString(), - to: tuple.to.toISOString(), - size, - filters: [], - primaryTimestamp, - secondaryTimestamp, - exceptionFilter, - }); - - ruleExecutionLogger.debug(`ES|QL query request: ${JSON.stringify(esqlRequest)}`); - const exceptionsWarning = getUnprocessedExceptionsWarnings(unprocessedExceptions); - if (exceptionsWarning) { - result.warningMessages.push(exceptionsWarning); - } + try { + while ( + result.createdSignalsCount <= tuple.maxSignals && + size <= ESQL_PAGE_SIZE_CIRCUIT_BREAKER + ) { + const esqlRequest = buildEsqlSearchRequest({ + query: ruleParams.query, + from: tuple.from.toISOString(), + to: tuple.to.toISOString(), + size, + filters: [], + primaryTimestamp, + secondaryTimestamp, + exceptionFilter, + }); - const esqlSignalSearchStart = performance.now(); + if (isLoggedRequestsEnabled) { + loggedRequests.push({ + request: logEsqlRequest(esqlRequest), + description: i18n.ESQL_SEARCH_REQUEST_DESCRIPTION, + }); + } - const response = await performEsqlRequest({ - esClient: services.scopedClusterClient.asCurrentUser, - requestParams: esqlRequest, - }); + ruleExecutionLogger.debug(`ES|QL query request: ${JSON.stringify(esqlRequest)}`); + const exceptionsWarning = getUnprocessedExceptionsWarnings(unprocessedExceptions); + if (exceptionsWarning) { + result.warningMessages.push(exceptionsWarning); + } - const esqlSearchDuration = makeFloatString(performance.now() - esqlSignalSearchStart); - result.searchAfterTimes.push(esqlSearchDuration); + const esqlSignalSearchStart = performance.now(); - ruleExecutionLogger.debug(`ES|QL query request took: ${esqlSearchDuration}ms`); + const response = await performEsqlRequest({ + esClient: services.scopedClusterClient.asCurrentUser, + requestParams: esqlRequest, + }); - const isRuleAggregating = computeIsESQLQueryAggregating(completeRule.ruleParams.query); + const esqlSearchDuration = performance.now() - esqlSignalSearchStart; + result.searchAfterTimes.push(makeFloatString(esqlSearchDuration)); - const results = response.values - // slicing already processed results in previous iterations - .slice(size - tuple.maxSignals) - .map((row) => rowToDocument(response.columns, row)); + if (isLoggedRequestsEnabled && loggedRequests[0]) { + loggedRequests[0].duration = Math.round(esqlSearchDuration); + } - const index = getIndexListFromEsqlQuery(completeRule.ruleParams.query); + ruleExecutionLogger.debug(`ES|QL query request took: ${esqlSearchDuration}ms`); - const sourceDocuments = await fetchSourceDocuments({ - esClient: services.scopedClusterClient.asCurrentUser, - results, - index, - isRuleAggregating, - }); + const isRuleAggregating = computeIsESQLQueryAggregating(completeRule.ruleParams.query); - const isAlertSuppressionActive = await getIsAlertSuppressionActive({ - alertSuppression: completeRule.ruleParams.alertSuppression, - licensing, - }); + const results = response.values + // slicing already processed results in previous iterations + .slice(size - tuple.maxSignals) + .map((row) => rowToDocument(response.columns, row)); - const wrapHits = (events: Array>) => - wrapEsqlAlerts({ - events, - spaceId, - completeRule, - mergeStrategy, + const index = getIndexListFromEsqlQuery(completeRule.ruleParams.query); + + const sourceDocuments = await fetchSourceDocuments({ + esClient: services.scopedClusterClient.asCurrentUser, + results, + index, isRuleAggregating, - alertTimestampOverride, - ruleExecutionLogger, - publicBaseUrl, - tuple, + loggedRequests: isLoggedRequestsEnabled ? loggedRequests : undefined, }); - const syntheticHits: Array> = results.map((document) => { - const { _id, _version, _index, ...source } = document; - - return { - _source: source as SignalSource, - fields: _id ? sourceDocuments[_id]?.fields : {}, - _id: _id ?? '', - _index: _index ?? '', - }; - }); + const isAlertSuppressionActive = await getIsAlertSuppressionActive({ + alertSuppression: completeRule.ruleParams.alertSuppression, + licensing, + }); - if (isAlertSuppressionActive) { - const wrapSuppressedHits = (events: Array>) => - wrapSuppressedEsqlAlerts({ + const wrapHits = (events: Array>) => + wrapEsqlAlerts({ events, spaceId, completeRule, @@ -173,78 +166,108 @@ export const esqlExecutor = async ({ alertTimestampOverride, ruleExecutionLogger, publicBaseUrl, - primaryTimestamp, - secondaryTimestamp, tuple, }); - const bulkCreateResult = await bulkCreateSuppressedAlertsInMemory({ - enrichedEvents: syntheticHits, - toReturn: result, - wrapHits, - bulkCreate, - services, - ruleExecutionLogger, - tuple, - alertSuppression: completeRule.ruleParams.alertSuppression, - wrapSuppressedHits, - alertTimestampOverride, - alertWithSuppression, - experimentalFeatures, - buildReasonMessage: buildReasonMessageForEsqlAlert, - mergeSourceAndFields: true, - // passing 1 here since ES|QL does not support pagination - maxNumberOfAlertsMultiplier: 1, + const syntheticHits: Array> = results.map((document) => { + const { _id, _version, _index, ...source } = document; + + return { + _source: source as SignalSource, + fields: _id ? sourceDocuments[_id]?.fields : {}, + _id: _id ?? '', + _index: _index ?? '', + }; }); - ruleExecutionLogger.debug( - `Created ${bulkCreateResult.createdItemsCount} alerts. Suppressed ${bulkCreateResult.suppressedItemsCount} alerts` - ); + if (isAlertSuppressionActive) { + const wrapSuppressedHits = (events: Array>) => + wrapSuppressedEsqlAlerts({ + events, + spaceId, + completeRule, + mergeStrategy, + isRuleAggregating, + alertTimestampOverride, + ruleExecutionLogger, + publicBaseUrl, + primaryTimestamp, + secondaryTimestamp, + tuple, + }); - if (bulkCreateResult.alertsWereTruncated) { - result.warningMessages.push(getSuppressionMaxSignalsWarning()); - break; - } - } else { - const wrappedAlerts = wrapHits(syntheticHits); + const bulkCreateResult = await bulkCreateSuppressedAlertsInMemory({ + enrichedEvents: syntheticHits, + toReturn: result, + wrapHits, + bulkCreate, + services, + ruleExecutionLogger, + tuple, + alertSuppression: completeRule.ruleParams.alertSuppression, + wrapSuppressedHits, + alertTimestampOverride, + alertWithSuppression, + experimentalFeatures, + buildReasonMessage: buildReasonMessageForEsqlAlert, + mergeSourceAndFields: true, + // passing 1 here since ES|QL does not support pagination + maxNumberOfAlertsMultiplier: 1, + }); - const enrichAlerts = createEnrichEventsFunction({ - services, - logger: ruleExecutionLogger, - }); - const bulkCreateResult = await bulkCreate( - wrappedAlerts, - tuple.maxSignals - result.createdSignalsCount, - enrichAlerts - ); + ruleExecutionLogger.debug( + `Created ${bulkCreateResult.createdItemsCount} alerts. Suppressed ${bulkCreateResult.suppressedItemsCount} alerts` + ); - addToSearchAfterReturn({ current: result, next: bulkCreateResult }); - ruleExecutionLogger.debug(`Created ${bulkCreateResult.createdItemsCount} alerts`); + if (bulkCreateResult.alertsWereTruncated) { + result.warningMessages.push(getSuppressionMaxSignalsWarning()); + break; + } + } else { + const wrappedAlerts = wrapHits(syntheticHits); - if (bulkCreateResult.alertsWereTruncated) { - result.warningMessages.push(getMaxSignalsWarning()); - break; + const enrichAlerts = createEnrichEventsFunction({ + services, + logger: ruleExecutionLogger, + }); + const bulkCreateResult = await bulkCreate( + wrappedAlerts, + tuple.maxSignals - result.createdSignalsCount, + enrichAlerts + ); + + addToSearchAfterReturn({ current: result, next: bulkCreateResult }); + ruleExecutionLogger.debug(`Created ${bulkCreateResult.createdItemsCount} alerts`); + + if (bulkCreateResult.alertsWereTruncated) { + result.warningMessages.push(getMaxSignalsWarning()); + break; + } + } + + if (scheduleNotificationResponseActionsService) { + scheduleNotificationResponseActionsService({ + signals: result.createdSignals, + signalsCount: result.createdSignalsCount, + responseActions: completeRule.ruleParams.responseActions, + }); } - } - if (scheduleNotificationResponseActionsService) { - scheduleNotificationResponseActionsService({ - signals: result.createdSignals, - signalsCount: result.createdSignalsCount, - responseActions: completeRule.ruleParams.responseActions, - }); - } - // no more results will be found - if (response.values.length < size) { - ruleExecutionLogger.debug( - `End of search: Found ${response.values.length} results with page size ${size}` - ); - break; + // no more results will be found + if (response.values.length < size) { + ruleExecutionLogger.debug( + `End of search: Found ${response.values.length} results with page size ${size}` + ); + break; + } + // ES|QL does not support pagination so we need to increase size of response to be able to catch all events + size += tuple.maxSignals; } - // ES|QL does not support pagination so we need to increase size of response to be able to catch all events - size += tuple.maxSignals; + } catch (error) { + result.errors.push(error.message); + result.success = false; } - return { ...result, state }; + return { ...result, state, ...(isLoggedRequestsEnabled ? { loggedRequests } : {}) }; }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/fetch_source_documents.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/fetch_source_documents.ts index 13828c0ed6770..7ee34183f0317 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/fetch_source_documents.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/fetch_source_documents.ts @@ -7,12 +7,16 @@ import type { ElasticsearchClient } from '@kbn/core/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/types'; +import type { RulePreviewLoggedRequest } from '../../../../../common/api/detection_engine/rule_preview/rule_preview.gen'; +import { logQueryRequest } from '../utils/logged_requests'; +import * as i18n from '../translations'; interface FetchSourceDocumentsArgs { isRuleAggregating: boolean; esClient: ElasticsearchClient; index: string[]; results: Array>; + loggedRequests?: RulePreviewLoggedRequest[]; } /** * fetches source documents by list of their ids @@ -24,6 +28,7 @@ export const fetchSourceDocuments = async ({ results, esClient, index, + loggedRequests, }: FetchSourceDocumentsArgs): Promise> => { const ids = results.reduce((acc, doc) => { if (doc._id) { @@ -47,16 +52,30 @@ export const fetchSourceDocuments = async ({ }, }; + const searchBody = { + query: idsQuery.query, + _source: false, + fields: ['*'], + }; + const ignoreUnavailable = true; + + if (loggedRequests) { + loggedRequests.push({ + request: logQueryRequest(searchBody, { index, ignoreUnavailable }), + description: i18n.FIND_SOURCE_DOCUMENTS_REQUEST_DESCRIPTION, + }); + } + const response = await esClient.search({ index, - body: { - query: idsQuery.query, - _source: false, - fields: ['*'], - }, - ignore_unavailable: true, + body: searchBody, + ignore_unavailable: ignoreUnavailable, }); + if (loggedRequests) { + loggedRequests[loggedRequests.length - 1].duration = response.took; + } + return response.hits.hits.reduce>( (acc, hit) => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/translations.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/translations.ts new file mode 100644 index 0000000000000..88445b0d41dc3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/translations.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ESQL_SEARCH_REQUEST_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.esqlRuleType.esqlSearchRequestDescription', + { + defaultMessage: 'ES|QL request to find all matches', + } +); + +export const FIND_SOURCE_DOCUMENTS_REQUEST_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.esqlRuleType.findSourceDocumentsRequestDescription', + { + defaultMessage: 'Retrieve source documents when ES|QL query is not aggregable', + } +); + +export const EQL_SEARCH_REQUEST_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.esqlRuleType.eqlSearchRequestDescription', + { + defaultMessage: 'EQL request to find all matches', + } +); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts index a29beef7bbb20..78c0a729be10e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts @@ -35,6 +35,7 @@ import type { TypeOfFieldMap } from '@kbn/rule-registry-plugin/common/field_map' import type { Filter, DataViewFieldBase } from '@kbn/es-query'; import type { LicensingPluginSetup } from '@kbn/licensing-plugin/server'; +import type { RulePreviewLoggedRequest } from '../../../../common/api/detection_engine/rule_preview/rule_preview.gen'; import type { RuleResponseAction } from '../../../../common/api/detection_engine/model/rule_response_actions'; import type { ConfigType } from '../../../config'; import type { SetupPlugins } from '../../../plugin'; @@ -74,6 +75,7 @@ export interface SecurityAlertTypeReturnValue { warning: boolean; warningMessages: string[]; suppressedAlertsCount?: number; + loggedRequests?: RulePreviewLoggedRequest[]; } export interface RunOpts { @@ -126,7 +128,12 @@ export type SecurityAlertType< services: PersistenceServices; runOpts: RunOpts; } - ) => Promise; + ) => Promise< + SearchAfterAndBulkCreateReturnType & { + state: TState; + loggedRequests?: RulePreviewLoggedRequest[]; + } + >; }; export interface CreateSecurityRuleTypeWrapperProps { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/index.ts new file mode 100644 index 0000000000000..7cea6e0d75fa6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/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 * from './log_esql'; +export * from './log_eql'; +export * from './log_query'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_eql.ts new file mode 100644 index 0000000000000..3f735e533b3b6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_eql.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EqlSearchRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +export const logEqlRequest = (request: EqlSearchRequest): string => { + const allowNoIndices = + request.allow_no_indices != null ? `?allow_no_indices=${request.allow_no_indices}` : ''; + + return `POST /${request.index}/_eql/search${allowNoIndices}\n${JSON.stringify( + request.body, + null, + 2 + )}`; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_esql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_esql.ts new file mode 100644 index 0000000000000..ea2d652dc5e5e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_esql.ts @@ -0,0 +1,15 @@ +/* + * 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +export const logEsqlRequest = (esqlRequest: { + query: string; + filter: QueryDslQueryContainer; +}): string => { + return `POST _query\n${JSON.stringify(esqlRequest, null, 2)}`; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_query.ts new file mode 100644 index 0000000000000..8ea992793e31d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_query.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + QueryDslQueryContainer, + SearchSourceConfig, + Indices, + Fields, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +interface SearchRequest { + query?: QueryDslQueryContainer; + _source?: SearchSourceConfig; + fields?: Fields; +} + +interface LogQueryRequestParams { + index: Indices; + ignoreUnavailable?: boolean; +} + +export const logQueryRequest = ( + searchRequest: SearchRequest, + { index, ignoreUnavailable = false }: LogQueryRequestParams +): string => { + return `POST /${index}/_search?ignore_unavailable=${ignoreUnavailable}\n${JSON.stringify( + searchRequest, + null, + 2 + )}`; +}; diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index cf9722e89b408..932a260127fd5 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -105,7 +105,10 @@ import { PreviewRiskScoreRequestBodyInput } from '@kbn/security-solution-plugin/ import { ReadAlertsMigrationStatusRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals_migration/read_signals_migration_status/read_signals_migration_status.gen'; import { ReadRuleRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/read_rule/read_rule_route.gen'; import { ResolveTimelineRequestQueryInput } from '@kbn/security-solution-plugin/common/api/timeline/resolve_timeline/resolve_timeline_route.gen'; -import { RulePreviewRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_preview/rule_preview.gen'; +import { + RulePreviewRequestQueryInput, + RulePreviewRequestBodyInput, +} from '@kbn/security-solution-plugin/common/api/detection_engine/rule_preview/rule_preview.gen'; import { SearchAlertsRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals/query_signals/query_signals_route.gen'; import { SetAlertAssigneesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/alert_assignees/set_alert_assignees_route.gen'; import { SetAlertsStatusRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals/set_signal_status/set_signals_status_route.gen'; @@ -959,7 +962,8 @@ detection engine rules. .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') - .send(props.body as object); + .send(props.body as object) + .query(props.query); }, scheduleRiskEngineNow() { return supertest @@ -1264,6 +1268,7 @@ export interface ResolveTimelineProps { query: ResolveTimelineRequestQueryInput; } export interface RulePreviewProps { + query: RulePreviewRequestQueryInput; body: RulePreviewRequestBodyInput; } export interface SearchAlertsProps { diff --git a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts index 6cd2702857e0f..705c0b8686dd0 100644 --- a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts +++ b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts @@ -82,6 +82,7 @@ export function createTestConfig(options: CreateTestConfigOptions, testFiles?: s '--xpack.ruleRegistry.unsafe.legacyMultiTenancy.enabled=true', `--xpack.securitySolution.enableExperimental=${JSON.stringify([ 'previewTelemetryUrlEnabled', + 'loggingRequestsEnabled', 'riskScoringPersistence', 'riskScoringRoutesEnabled', 'manualRuleRunEnabled', diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts index 6691a781d4e1e..8f64a859b7002 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts @@ -17,6 +17,9 @@ export default createTestConfig({ 'testing_ignored.constant', '/testing_regex*/', ])}`, // See tests within the file "ignore_fields.ts" which use these values in "alertIgnoreFields" - `--xpack.securitySolution.enableExperimental=${JSON.stringify(['manualRuleRunEnabled'])}`, + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'manualRuleRunEnabled', + 'loggingRequestsEnabled', + ])}`, ], }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts index 53b2843399c62..aff2ccc6bccb3 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts @@ -1189,5 +1189,32 @@ export default ({ getService }: FtrProviderContext) => { expect(updatedAlerts.hits.hits[0]._source?.[ALERT_SUPPRESSION_DOCS_COUNT]).equal(1); }); }); + + // skipped on MKI since feature flags are not supported there + describe('@skipInServerlessMKI preview logged requests', () => { + it('should not return requests property when not enabled', async () => { + const { logs } = await previewRule({ + supertest, + rule: getEqlRuleForAlertTesting(['auditbeat-*']), + }); + + expect(logs[0].requests).equal(undefined); + }); + it('should return requests property when enable_logged_requests set to true', async () => { + const { logs } = await previewRule({ + supertest, + rule: getEqlRuleForAlertTesting(['auditbeat-*']), + enableLoggedRequests: true, + }); + + const requests = logs[0].requests; + + expect(requests).to.have.length(1); + expect(requests![0].description).to.be('EQL request to find all matches'); + expect(requests![0].request).to.contain( + 'POST /auditbeat-*/_eql/search?allow_no_indices=true' + ); + }); + }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts index 9fbda25bdae64..166a62b9b08ad 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts @@ -1408,5 +1408,63 @@ export default ({ getService }: FtrProviderContext) => { }); }); }); + + // skipped on MKI since feature flags are not supported there + describe('@skipInServerlessMKI preview logged requests', () => { + let rule: EsqlRuleCreateProps; + let id: string; + beforeEach(async () => { + id = uuidv4(); + const interval: [string, string] = ['2020-10-28T06:00:00.000Z', '2020-10-28T06:10:00.000Z']; + const doc1 = { agent: { name: 'test-1' } }; + + rule = { + ...getCreateEsqlRulesSchemaMock('rule-1', true), + query: `from ecs_compliant metadata _id ${internalIdPipe( + id + )} | where agent.name=="test-1"`, + from: 'now-1h', + interval: '1h', + }; + + await indexEnhancedDocuments({ documents: [doc1], interval, id }); + }); + + it('should not return requests property when not enabled', async () => { + const { logs } = await previewRule({ + supertest, + rule, + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + }); + + expect(logs[0]).not.toHaveProperty('requests'); + }); + it('should return requests property when enable_logged_requests set to true', async () => { + const { logs } = await previewRule({ + supertest, + rule, + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + enableLoggedRequests: true, + }); + + const requests = logs[0].requests; + expect(requests).toHaveLength(2); + + expect(requests).toHaveProperty('0.description', 'ES|QL request to find all matches'); + expect(requests).toHaveProperty('0.duration', expect.any(Number)); + expect(requests![0].request).toContain( + `"query": "from ecs_compliant metadata _id | where id==\\\"${id}\\\" | where agent.name==\\\"test-1\\\" | limit 101",` + ); + + expect(requests).toHaveProperty( + '1.description', + 'Retrieve source documents when ES|QL query is not aggregable' + ); + expect(requests).toHaveProperty('1.duration', expect.any(Number)); + expect(requests![1].request).toContain( + 'POST /ecs_compliant/_search?ignore_unavailable=true' + ); + }); + }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/preview_rule.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/preview_rule.ts index a601edde98168..79668b902f5d0 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/preview_rule.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/preview_rule.ts @@ -26,11 +26,13 @@ export const previewRule = async ({ rule, invocationCount = 1, timeframeEnd = new Date(), + enableLoggedRequests, }: { supertest: SuperTest.Agent; rule: RuleCreateProps; invocationCount?: number; timeframeEnd?: Date; + enableLoggedRequests?: boolean; }): Promise<{ previewId: string; logs: RulePreviewLogs[]; @@ -43,6 +45,7 @@ export const previewRule = async ({ }; const response = await supertest .post(DETECTION_ENGINE_RULES_PREVIEW) + .query(enableLoggedRequests ? { enable_logged_requests: true } : {}) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') .send(previewRequest) diff --git a/x-pack/test/security_solution_cypress/config.ts b/x-pack/test/security_solution_cypress/config.ts index 58d873369d99d..88752eb1b5f93 100644 --- a/x-pack/test/security_solution_cypress/config.ts +++ b/x-pack/test/security_solution_cypress/config.ts @@ -44,7 +44,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { // See https://github.com/elastic/kibana/pull/125396 for details '--xpack.alerting.rules.minimumScheduleInterval.value=1s', '--xpack.ruleRegistry.unsafe.legacyMultiTenancy.enabled=true', - `--xpack.securitySolution.enableExperimental=${JSON.stringify(['manualRuleRunEnabled'])}`, + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'manualRuleRunEnabled', + 'loggingRequestsEnabled', + ])}`, // mock cloud to enable the guided onboarding tour in e2e tests '--xpack.cloud.id=test', `--home.disableWelcomeScreen=true`, diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/preview.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/preview.cy.ts new file mode 100644 index 0000000000000..ce298bafbfea0 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/preview.cy.ts @@ -0,0 +1,85 @@ +/* + * 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 { getEsqlRule, getSimpleCustomQueryRule } from '../../../../objects/rule'; + +import { + PREVIEW_LOGGED_REQUEST_DESCRIPTION, + PREVIEW_LOGGED_REQUEST_CODE_BLOCK, + PREVIEW_LOGGED_REQUESTS_CHECKBOX, + RULES_CREATION_PREVIEW_REFRESH_BUTTON, +} from '../../../../screens/create_new_rule'; + +import { createRule } from '../../../../tasks/api_calls/rules'; + +import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; +import { + checkEnableLoggedRequests, + submitRulePreview, + toggleLoggedRequestsAccordion, + toggleLoggedRequestsItemAccordion, +} from '../../../../tasks/create_new_rule'; +import { login } from '../../../../tasks/login'; + +import { visitEditRulePage } from '../../../../tasks/edit_rule'; + +const expectedValidEsqlQuery = 'from auditbeat* METADATA _id'; + +describe( + 'Detection rules, preview', + { + // Currently FF are not supported on MKI environments, so this test should be skipped from MKI environments. + // Once `manualRuleRunEnabled` FF is removed, we can remove `@skipInServerlessMKI` as well + tags: ['@ess', '@serverless', '@skipInServerlessMKI'], + env: { + kbnServerArgs: [ + `--xpack.securitySolution.enableExperimental=${JSON.stringify(['loggingRequestsEnabled'])}`, + ], + }, + }, + () => { + beforeEach(() => { + login(); + deleteAlertsAndRules(); + }); + + describe('supports preview logged requests', () => { + beforeEach(() => { + createRule({ ...getEsqlRule(), query: expectedValidEsqlQuery }).then((createdRule) => { + visitEditRulePage(createdRule.body.id); + }); + }); + + it('shows preview logged requests', () => { + checkEnableLoggedRequests(); + submitRulePreview(); + + toggleLoggedRequestsAccordion(); + toggleLoggedRequestsItemAccordion(); + + cy.get(PREVIEW_LOGGED_REQUEST_DESCRIPTION) + .first() + .contains('ES|QL request to find all matches'); + + cy.get(PREVIEW_LOGGED_REQUEST_CODE_BLOCK).first().contains(expectedValidEsqlQuery); + }); + }); + + describe('does not support preview logged requests', () => { + beforeEach(() => { + createRule(getSimpleCustomQueryRule()).then((createdRule) => { + visitEditRulePage(createdRule.body.id); + }); + }); + + it('does not show preview logged requests checkbox', () => { + cy.get(RULES_CREATION_PREVIEW_REFRESH_BUTTON).should('be.visible'); + cy.get(PREVIEW_LOGGED_REQUESTS_CHECKBOX).should('not.exist'); + }); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts b/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts index 72d1104985d77..a191fc22aa339 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts @@ -296,3 +296,19 @@ export const RULE_INDICES = '[data-test-subj="detectionEngineStepDefineRuleIndices"] [data-test-subj="comboBoxInput"]'; export const ALERTS_INDEX_BUTTON = 'span[title=".alerts-security.alerts-default"] button'; + +export const PREVIEW_SUBMIT_BUTTON = '[data-test-subj="previewSubmitButton"]'; + +export const PREVIEW_LOGGED_REQUESTS_CHECKBOX = '[data-test-subj="show-elasticsearch-requests"]'; + +export const PREVIEW_LOGGED_REQUESTS_ACCORDION_BUTTON = + '[data-test-subj="preview-logged-requests-accordion"] button'; + +export const PREVIEW_LOGGED_REQUESTS_ITEM_ACCORDION_BUTTON = + '[data-test-subj="preview-logged-requests-item-accordion"] button'; + +export const PREVIEW_LOGGED_REQUEST_DESCRIPTION = + '[data-test-subj="preview-logged-request-description"]'; + +export const PREVIEW_LOGGED_REQUEST_CODE_BLOCK = + '[data-test-subj="preview-logged-request-code-block"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts index ecc37de80b456..68dc2cfffd908 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts @@ -130,6 +130,9 @@ import { RELATED_INTEGRATION_COMBO_BOX_INPUT, SAVE_WITH_ERRORS_MODAL, SAVE_WITH_ERRORS_MODAL_CONFIRM_BTN, + PREVIEW_LOGGED_REQUESTS_ACCORDION_BUTTON, + PREVIEW_LOGGED_REQUESTS_ITEM_ACCORDION_BUTTON, + PREVIEW_LOGGED_REQUESTS_CHECKBOX, } from '../screens/create_new_rule'; import { INDEX_SELECTOR, @@ -996,3 +999,20 @@ export const uncheckLoadQueryDynamically = () => { export const openAddFilterPopover = () => { cy.get(QUERY_BAR_ADD_FILTER).click(); }; + +export const checkEnableLoggedRequests = () => { + cy.get(PREVIEW_LOGGED_REQUESTS_CHECKBOX).click(); + cy.get(PREVIEW_LOGGED_REQUESTS_CHECKBOX).should('be.checked'); +}; + +export const submitRulePreview = () => { + cy.get(RULES_CREATION_PREVIEW_REFRESH_BUTTON).click(); +}; + +export const toggleLoggedRequestsAccordion = () => { + cy.get(PREVIEW_LOGGED_REQUESTS_ACCORDION_BUTTON).first().click(); +}; + +export const toggleLoggedRequestsItemAccordion = () => { + cy.get(PREVIEW_LOGGED_REQUESTS_ITEM_ACCORDION_BUTTON).should('be.visible').first().click(); +}; diff --git a/x-pack/test/security_solution_cypress/serverless_config.ts b/x-pack/test/security_solution_cypress/serverless_config.ts index 226f2db6dbbc0..13877fcbf5af4 100644 --- a/x-pack/test/security_solution_cypress/serverless_config.ts +++ b/x-pack/test/security_solution_cypress/serverless_config.ts @@ -34,7 +34,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { { product_line: 'endpoint', product_tier: 'complete' }, { product_line: 'cloud', product_tier: 'complete' }, ])}`, - `--xpack.securitySolution.enableExperimental=${JSON.stringify(['manualRuleRunEnabled'])}`, + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'manualRuleRunEnabled', + 'loggingRequestsEnabled', + ])}`, '--csp.strict=false', '--csp.warnLegacyBrowsers=false', ], From 2867b18d3547942021dd7be14a3595f6e5a78d36 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 20 Sep 2024 01:27:01 +1000 Subject: [PATCH 11/19] [8.x] [kbn-expandable-flyout] - add support for resizable flyout (#192906) (#193371) # Backport This will backport the following commits from `main` to `8.x`: - [[kbn-expandable-flyout] - add support for resizable flyout (#192906)](https://github.com/elastic/kibana/pull/192906) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Philippe Oberti Co-authored-by: Elastic Machine --- .../src/components/container.test.tsx | 180 +++++++++ .../src/components/container.tsx | 241 ++++++++++++ .../src/components/left_section.tsx | 24 +- .../src/components/preview_section.test.tsx | 11 +- .../src/components/preview_section.tsx | 28 +- .../components/resizable_container.test.tsx | 51 +++ .../src/components/resizable_container.tsx | 116 ++++++ .../src/components/right_section.tsx | 23 +- .../src/components/settings_menu.test.tsx | 362 ++++++++++++------ .../src/components/settings_menu.tsx | 55 ++- .../src/components/test_ids.ts | 15 + .../kbn-expandable-flyout/src/constants.ts | 3 + .../src/hooks/use_expandable_flyout_state.ts | 4 +- .../use_initialize_from_local_storage.test.ts | 55 ++- .../use_initialize_from_local_storage.ts | 52 ++- .../src/hooks/use_sections.test.tsx | 134 +++++++ .../src/hooks/use_sections.ts | 86 +++++ .../src/hooks/use_sections_sizes.test.ts | 251 ------------ .../src/hooks/use_sections_sizes.ts | 114 ------ .../src/hooks/use_window_size.test.ts | 18 - .../src/hooks/use_window_size.ts | 26 -- .../src/hooks/use_window_width.test.ts | 150 ++++++++ .../src/hooks/use_window_width.ts | 84 ++++ .../src/index.stories.tsx | 83 +++- .../kbn-expandable-flyout/src/index.test.tsx | 148 +------ packages/kbn-expandable-flyout/src/index.tsx | 125 +----- .../src/provider.test.tsx | 14 +- .../src/store/actions.ts | 63 +++ .../src/store/middlewares.test.ts | 205 ++++++++-- .../src/store/middlewares.ts | 106 ++++- .../src/store/reducers.test.ts | 238 ++++++++++++ .../src/store/reducers.ts | 34 ++ .../kbn-expandable-flyout/src/store/redux.ts | 20 +- .../kbn-expandable-flyout/src/store/state.ts | 64 ++++ .../src/test/provider.tsx | 12 +- 35 files changed, 2288 insertions(+), 907 deletions(-) create mode 100644 packages/kbn-expandable-flyout/src/components/container.test.tsx create mode 100644 packages/kbn-expandable-flyout/src/components/container.tsx create mode 100644 packages/kbn-expandable-flyout/src/components/resizable_container.test.tsx create mode 100644 packages/kbn-expandable-flyout/src/components/resizable_container.tsx create mode 100644 packages/kbn-expandable-flyout/src/hooks/use_sections.test.tsx create mode 100644 packages/kbn-expandable-flyout/src/hooks/use_sections.ts delete mode 100644 packages/kbn-expandable-flyout/src/hooks/use_sections_sizes.test.ts delete mode 100644 packages/kbn-expandable-flyout/src/hooks/use_sections_sizes.ts delete mode 100644 packages/kbn-expandable-flyout/src/hooks/use_window_size.test.ts delete mode 100644 packages/kbn-expandable-flyout/src/hooks/use_window_size.ts create mode 100644 packages/kbn-expandable-flyout/src/hooks/use_window_width.test.ts create mode 100644 packages/kbn-expandable-flyout/src/hooks/use_window_width.ts diff --git a/packages/kbn-expandable-flyout/src/components/container.test.tsx b/packages/kbn-expandable-flyout/src/components/container.test.tsx new file mode 100644 index 0000000000000..fa27d81fa4437 --- /dev/null +++ b/packages/kbn-expandable-flyout/src/components/container.test.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { render } from '@testing-library/react'; + +import { Panel } from '../types'; +import { + LEFT_SECTION_TEST_ID, + PREVIEW_SECTION_TEST_ID, + SETTINGS_MENU_BUTTON_TEST_ID, + RIGHT_SECTION_TEST_ID, +} from './test_ids'; +import { initialUiState, type State } from '../store/state'; +import { TestProvider } from '../test/provider'; +import { REDUX_ID_FOR_MEMORY_STORAGE } from '../constants'; +import { Container } from './container'; + +const id = REDUX_ID_FOR_MEMORY_STORAGE; +const registeredPanels: Panel[] = [ + { + key: 'key', + component: () =>
{'component'}
, + }, +]; + +describe('Container', () => { + it(`shouldn't render flyout if no panels`, () => { + const state: State = { + panels: { + byId: {}, + }, + ui: initialUiState, + }; + + const result = render( + + + + ); + + expect(result.asFragment()).toMatchInlineSnapshot(``); + }); + + it('should render collapsed flyout (right section)', () => { + const state: State = { + panels: { + byId: { + [id]: { + right: { + id: 'key', + }, + left: undefined, + preview: undefined, + }, + }, + }, + ui: initialUiState, + }; + + const { getByTestId } = render( + + + + ); + + expect(getByTestId(RIGHT_SECTION_TEST_ID)).toBeInTheDocument(); + }); + + it('should render expanded flyout (right and left sections)', () => { + const state: State = { + panels: { + byId: { + [id]: { + right: { + id: 'key', + }, + left: { + id: 'key', + }, + preview: undefined, + }, + }, + }, + ui: initialUiState, + }; + + const { getByTestId } = render( + + + + ); + + expect(getByTestId(LEFT_SECTION_TEST_ID)).toBeInTheDocument(); + }); + + it('should render preview section', () => { + const state: State = { + panels: { + byId: { + [id]: { + right: undefined, + left: undefined, + preview: [ + { + id: 'key', + }, + ], + }, + }, + }, + ui: initialUiState, + }; + + const { getByTestId } = render( + + + + ); + + expect(getByTestId(PREVIEW_SECTION_TEST_ID)).toBeInTheDocument(); + }); + + it('should not render flyout when right has value but does not matches registered panels', () => { + const state: State = { + panels: { + byId: { + [id]: { + right: { + id: 'key1', + }, + left: undefined, + preview: undefined, + }, + }, + }, + ui: initialUiState, + }; + + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId('my-test-flyout')).toBeNull(); + expect(queryByTestId(RIGHT_SECTION_TEST_ID)).toBeNull(); + }); + + it('should render the menu to change display options', () => { + const state: State = { + panels: { + byId: { + [id]: { + right: { + id: 'key', + }, + left: undefined, + preview: undefined, + }, + }, + }, + ui: initialUiState, + }; + + const { getByTestId } = render( + + + + ); + + expect(getByTestId(SETTINGS_MENU_BUTTON_TEST_ID)).toBeInTheDocument(); + }); +}); diff --git a/packages/kbn-expandable-flyout/src/components/container.tsx b/packages/kbn-expandable-flyout/src/components/container.tsx new file mode 100644 index 0000000000000..9d858d08be23c --- /dev/null +++ b/packages/kbn-expandable-flyout/src/components/container.tsx @@ -0,0 +1,241 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React, { memo, useCallback, useMemo } from 'react'; +import { Interpolation, Theme } from '@emotion/react'; +import { EuiFlyoutProps, EuiFlyoutResizable } from '@elastic/eui'; +import { EuiFlyoutResizableProps } from '@elastic/eui/src/components/flyout/flyout_resizable'; +import { changeUserCollapsedWidthAction, changeUserExpandedWidthAction } from '../store/actions'; +import { + selectDefaultWidths, + selectPushVsOverlay, + selectUserFlyoutWidths, + useDispatch, + useSelector, +} from '../store/redux'; +import { RightSection } from './right_section'; +import { useSections } from '../hooks/use_sections'; +import { useExpandableFlyoutState } from '../hooks/use_expandable_flyout_state'; +import { useExpandableFlyoutApi } from '../hooks/use_expandable_flyout_api'; +import type { FlyoutPanelProps, Panel } from '../types'; +import { SettingsMenu } from './settings_menu'; +import { PreviewSection } from './preview_section'; +import { ResizableContainer } from './resizable_container'; + +const COLLAPSED_FLYOUT_MIN_WIDTH = 380; +const EXPANDED_FLYOUT_MIN_WIDTH = 740; + +export interface ContainerProps extends Omit { + /** + * List of all registered panels available for render + */ + registeredPanels: Panel[]; + /** + * Allows for custom styles to be passed to the EuiFlyout component + */ + customStyles?: Interpolation; + /** + * Callback function to let application's code the flyout is closed + */ + onClose?: EuiFlyoutProps['onClose']; + /** + * Set of properties that drive a settings menu + */ + flyoutCustomProps?: { + /** + * Hide the gear icon and settings menu if true + */ + hideSettings?: boolean; + /** + * Control if the option to render in overlay or push mode is enabled or not + */ + pushVsOverlay?: { + /** + * Disables the option + */ + disabled: boolean; + /** + * Tooltip to display + */ + tooltip: string; + }; + /** + * Control if the option to resize the flyout is enabled or not + */ + resize?: { + /** + * Disables the option + */ + disabled: boolean; + /** + * Tooltip to display + */ + tooltip: string; + }; + }; + /** + * Optional data test subject string + */ + 'data-test-subj'?: string; +} + +/** + * Expandable flyout UI React component. + * Displays 3 sections (right, left, preview) depending on the panels in the context. + * + * The behavior expects that the left and preview sections should only be displayed is a right section + * is already rendered. + */ +export const Container: React.FC = memo( + ({ customStyles, registeredPanels, flyoutCustomProps, ...flyoutProps }) => { + const dispatch = useDispatch(); + + const { left, right, preview } = useExpandableFlyoutState(); + const { closeFlyout } = useExpandableFlyoutApi(); + + // for flyout where the push vs overlay option is disable in the UI we fall back to overlay mode + const type = useSelector(selectPushVsOverlay); + const flyoutType = flyoutCustomProps?.pushVsOverlay?.disabled ? 'overlay' : type; + + const flyoutWidths = useSelector(selectUserFlyoutWidths); + const defaultWidths = useSelector(selectDefaultWidths); + + // retrieves the sections to be displayed + const { + leftSection, + rightSection, + previewSection, + mostRecentPreview, + mostRecentPreviewBanner, + } = useSections({ + registeredPanels, + }); + + // calculates what needs to be rendered + const showLeft = useMemo(() => leftSection != null && left != null, [leftSection, left]); + const showRight = useMemo(() => rightSection != null && right != null, [rightSection, right]); + const showPreview = useMemo( + () => previewSection != null && preview != null, + [previewSection, preview] + ); + + const showCollapsed = useMemo(() => !showLeft && showRight, [showLeft, showRight]); + const showExpanded = useMemo(() => showLeft && showRight, [showLeft, showRight]); + + const leftComponent = useMemo( + () => (leftSection ? leftSection.component({ ...(left as FlyoutPanelProps) }) : null), + [leftSection, left] + ); + const rightComponent = useMemo( + () => (rightSection ? rightSection.component({ ...(right as FlyoutPanelProps) }) : null), + [rightSection, right] + ); + + const previewComponent = useMemo( + () => + previewSection + ? previewSection.component({ + ...(mostRecentPreview as FlyoutPanelProps), + }) + : null, + [previewSection, mostRecentPreview] + ); + + // we want to set a minimum flyout width different when in collapsed and expanded mode + const minFlyoutWidth = useMemo( + () => (showExpanded ? EXPANDED_FLYOUT_MIN_WIDTH : COLLAPSED_FLYOUT_MIN_WIDTH), + [showExpanded] + ); + + const flyoutWidth = useMemo(() => { + if (showCollapsed) { + return flyoutWidths.collapsedWidth || defaultWidths.rightWidth; + } + if (showExpanded) { + return flyoutWidths.expandedWidth || defaultWidths.rightWidth + defaultWidths.leftWidth; + } + }, [ + showCollapsed, + showExpanded, + flyoutWidths.collapsedWidth, + flyoutWidths.expandedWidth, + defaultWidths.rightWidth, + defaultWidths.leftWidth, + ]); + + // callback function called when user changes the flyout's width + const onResize = useCallback( + (width: number) => { + if (showExpanded) { + dispatch( + changeUserExpandedWidthAction({ + width, + savedToLocalStorage: true, + }) + ); + } else if (showCollapsed) { + dispatch( + changeUserCollapsedWidthAction({ + width, + savedToLocalStorage: true, + }) + ); + } + }, + [dispatch, showCollapsed, showExpanded] + ); + + // don't need to render if the windowWidth is 0 or if nothing needs to be rendered + if (!showExpanded && !showCollapsed && !showPreview) { + return null; + } + + return ( + // @ts-ignore // TODO figure out why it's throwing a 'Types of property ref are incompatible' error + { + closeFlyout(); + if (flyoutProps.onClose) { + flyoutProps.onClose(e); + } + }} + css={customStyles} + onResize={onResize} + minWidth={minFlyoutWidth} + > + {showCollapsed && } + + {showExpanded && ( + + )} + + {showPreview && ( + + )} + + {!flyoutCustomProps?.hideSettings && } + + ); + } +); + +Container.displayName = 'Container'; diff --git a/packages/kbn-expandable-flyout/src/components/left_section.tsx b/packages/kbn-expandable-flyout/src/components/left_section.tsx index c0bd285e9b162..591062116a971 100644 --- a/packages/kbn-expandable-flyout/src/components/left_section.tsx +++ b/packages/kbn-expandable-flyout/src/components/left_section.tsx @@ -8,7 +8,7 @@ */ import { EuiFlexItem } from '@elastic/eui'; -import React, { memo, useMemo } from 'react'; +import React, { memo } from 'react'; import { LEFT_SECTION_TEST_ID } from './test_ids'; interface LeftSectionProps { @@ -16,27 +16,15 @@ interface LeftSectionProps { * Component to be rendered */ component: React.ReactElement; - /** - * Width used when rendering the panel - */ - width: number; } /** * Left section of the expanded flyout rendering a panel */ -export const LeftSection: React.FC = memo( - ({ component, width }: LeftSectionProps) => { - const style = useMemo( - () => ({ height: '100%', width: `${width}px` }), - [width] - ); - return ( - - {component} - - ); - } -); +export const LeftSection: React.FC = memo(({ component }: LeftSectionProps) => ( + + {component} + +)); LeftSection.displayName = 'LeftSection'; diff --git a/packages/kbn-expandable-flyout/src/components/preview_section.test.tsx b/packages/kbn-expandable-flyout/src/components/preview_section.test.tsx index 9916f2e784dfa..6476ac91c0031 100644 --- a/packages/kbn-expandable-flyout/src/components/preview_section.test.tsx +++ b/packages/kbn-expandable-flyout/src/components/preview_section.test.tsx @@ -16,7 +16,7 @@ import { PREVIEW_SECTION_TEST_ID, } from './test_ids'; import { TestProvider } from '../test/provider'; -import { State } from '../store/state'; +import { initialUiState, State } from '../store/state'; describe('PreviewSection', () => { const context: State = { @@ -33,18 +33,15 @@ describe('PreviewSection', () => { }, }, }, - ui: { - pushVsOverlay: 'overlay', - }, + ui: initialUiState, }; const component =
{'component'}
; - const left = 500; it('should render back button and close button in header', () => { const { getByTestId } = render( - + ); @@ -62,7 +59,7 @@ describe('PreviewSection', () => { const { getByTestId, getByText } = render( - + ); diff --git a/packages/kbn-expandable-flyout/src/components/preview_section.tsx b/packages/kbn-expandable-flyout/src/components/preview_section.tsx index f461c8c3710bf..d759e5500534b 100644 --- a/packages/kbn-expandable-flyout/src/components/preview_section.tsx +++ b/packages/kbn-expandable-flyout/src/components/preview_section.tsx @@ -17,9 +17,10 @@ import { EuiSplitPanel, transparentize, } from '@elastic/eui'; -import React, { memo } from 'react'; +import React, { memo, useMemo } from 'react'; import { css } from '@emotion/react'; import { has } from 'lodash'; +import { selectDefaultWidths, selectUserSectionWidths, useSelector } from '../store/redux'; import { PREVIEW_SECTION_BACK_BUTTON_TEST_ID, PREVIEW_SECTION_CLOSE_BUTTON_TEST_ID, @@ -66,14 +67,14 @@ interface PreviewSectionProps { * Component to be rendered */ component: React.ReactElement; - /** - * Left position used when rendering the panel - */ - leftPosition: number; /** * Preview banner shown at the top of preview panel */ banner?: PreviewBanner; + /** + * Flag to indicate whether the preview section is expanded, use to calculate the width of the section + */ + showExpanded: boolean; } /** @@ -81,11 +82,20 @@ interface PreviewSectionProps { * Will display a back and close button in the header for the previous and close feature respectively. */ export const PreviewSection: React.FC = memo( - ({ component, leftPosition, banner }: PreviewSectionProps) => { + ({ component, banner, showExpanded }: PreviewSectionProps) => { const { euiTheme } = useEuiTheme(); const { closePreviewPanel, previousPreviewPanel } = useExpandableFlyoutApi(); - const left = leftPosition + 4; + const { rightPercentage } = useSelector(selectUserSectionWidths); + const defaultPercentages = useSelector(selectDefaultWidths); + + // Calculate the width of the preview section based on the following + // - if only the right section is visible, then we use 100% of the width (minus some padding) + // - if both the right and left sections are visible, we use the width of the right section (minus the same padding) + const width = useMemo(() => { + const percentage = rightPercentage ? rightPercentage : defaultPercentages.rightPercentage; + return showExpanded ? `calc(${percentage}% - 8px)` : `calc(100% - 8px)`; + }, [defaultPercentages.rightPercentage, rightPercentage, showExpanded]); const closeButton = ( @@ -122,14 +132,14 @@ export const PreviewSection: React.FC = memo( top: 8px; bottom: 8px; right: 4px; - left: ${left}px; + width: ${width}; z-index: 1000; `} > {'left component'}; +const rightComponent =
{'right component'}
; + +describe('ResizableContainer', () => { + it('should render left and right component as well as resize button', () => { + const state = { + ...initialState, + ui: { + ...initialState.ui, + userSectionWidths: { + leftPercentage: 50, + rightPercentage: 50, + }, + }, + }; + + const { getByTestId } = render( + + + + ); + + expect(getByTestId(RESIZABLE_LEFT_SECTION_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(RESIZABLE_BUTTON_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(RESIZABLE_RIGHT_SECTION_TEST_ID)).toBeInTheDocument(); + }); +}); diff --git a/packages/kbn-expandable-flyout/src/components/resizable_container.tsx b/packages/kbn-expandable-flyout/src/components/resizable_container.tsx new file mode 100644 index 0000000000000..c7da40167a7fd --- /dev/null +++ b/packages/kbn-expandable-flyout/src/components/resizable_container.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { EuiResizableContainer } from '@elastic/eui'; +import React, { memo, useCallback, useMemo } from 'react'; +import { css } from '@emotion/react'; +import { changeUserSectionWidthsAction } from '../store/actions'; +import { + selectDefaultWidths, + selectUserSectionWidths, + useDispatch, + useSelector, +} from '../store/redux'; +import { + RESIZABLE_BUTTON_TEST_ID, + RESIZABLE_LEFT_SECTION_TEST_ID, + RESIZABLE_RIGHT_SECTION_TEST_ID, +} from './test_ids'; +import { LeftSection } from './left_section'; +import { RightSection } from './right_section'; + +const RIGHT_SECTION_MIN_WIDTH = '380px'; +const LEFT_SECTION_MIN_WIDTH = '380px'; +const LEFT_PANEL_ID = 'left'; +const RIGHT_PANEL_ID = 'right'; + +interface ResizableContainerProps { + /** + * The component to render on the left side of the flyout + */ + leftComponent: React.ReactElement; + /** + * The component to render on the right side of the flyout + */ + rightComponent: React.ReactElement; + /** + * If the preview section is shown we disable the resize button + */ + showPreview: boolean; +} + +/** + * Component that renders the left and right section when the flyout is in expanded mode. + * It allows the resizing of the sections, saving the percentages in local storage. + */ +export const ResizableContainer: React.FC = memo( + ({ leftComponent, rightComponent, showPreview }: ResizableContainerProps) => { + const dispatch = useDispatch(); + + const { leftPercentage, rightPercentage } = useSelector(selectUserSectionWidths); + const defaultPercentages = useSelector(selectDefaultWidths); + + const initialLeftPercentage = useMemo( + () => leftPercentage || defaultPercentages.leftPercentage, + [defaultPercentages.leftPercentage, leftPercentage] + ); + const initialRightPercentage = useMemo( + () => rightPercentage || defaultPercentages.rightPercentage, + [defaultPercentages.rightPercentage, rightPercentage] + ); + + const onWidthChange = useCallback( + (newSizes) => + dispatch( + changeUserSectionWidthsAction({ + ...newSizes, + savedToLocalStorage: true, + }) + ), + [dispatch] + ); + + return ( + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + + + + + + + + )} + + ); + } +); + +ResizableContainer.displayName = 'ResizableContainer'; diff --git a/packages/kbn-expandable-flyout/src/components/right_section.tsx b/packages/kbn-expandable-flyout/src/components/right_section.tsx index 73931e44ad5fe..ab6598b9f8e3a 100644 --- a/packages/kbn-expandable-flyout/src/components/right_section.tsx +++ b/packages/kbn-expandable-flyout/src/components/right_section.tsx @@ -8,7 +8,7 @@ */ import { EuiFlexItem } from '@elastic/eui'; -import React, { memo, useMemo } from 'react'; +import React, { memo } from 'react'; import { RIGHT_SECTION_TEST_ID } from './test_ids'; interface RightSectionProps { @@ -16,28 +16,17 @@ interface RightSectionProps { * Component to be rendered */ component: React.ReactElement; - /** - * Width used when rendering the panel - */ - width: number; } /** * Right section of the expanded flyout rendering a panel */ export const RightSection: React.FC = memo( - ({ component, width }: RightSectionProps) => { - const style = useMemo( - () => ({ height: '100%', width: `${width}px` }), - [width] - ); - - return ( - - {component} - - ); - } + ({ component }: RightSectionProps) => ( + + {component} + + ) ); RightSection.displayName = 'RightSection'; diff --git a/packages/kbn-expandable-flyout/src/components/settings_menu.test.tsx b/packages/kbn-expandable-flyout/src/components/settings_menu.test.tsx index 9ef33a649671d..f9a6991f55b52 100644 --- a/packages/kbn-expandable-flyout/src/components/settings_menu.test.tsx +++ b/packages/kbn-expandable-flyout/src/components/settings_menu.test.tsx @@ -13,6 +13,9 @@ import { render } from '@testing-library/react'; import { SettingsMenu } from './settings_menu'; import { SETTINGS_MENU_BUTTON_TEST_ID, + SETTINGS_MENU_FLYOUT_RESIZE_BUTTON_TEST_ID, + SETTINGS_MENU_FLYOUT_RESIZE_INFORMATION_ICON_TEST_ID, + SETTINGS_MENU_FLYOUT_RESIZE_TITLE_TEST_ID, SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_OVERLAY_TEST_ID, SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_PUSH_TEST_ID, SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_TEST_ID, @@ -21,8 +24,14 @@ import { } from './test_ids'; import { TestProvider } from '../test/provider'; import { localStorageMock } from '../../__mocks__'; -import { EXPANDABLE_FLYOUT_LOCAL_STORAGE, PUSH_VS_OVERLAY_LOCAL_STORAGE } from '../constants'; -import { initialPanelsState } from '../store/state'; +import { + USER_COLLAPSED_WIDTH_LOCAL_STORAGE, + EXPANDABLE_FLYOUT_LOCAL_STORAGE, + USER_EXPANDED_WIDTH_LOCAL_STORAGE, + USER_SECTION_WIDTHS_LOCAL_STORAGE, + PUSH_VS_OVERLAY_LOCAL_STORAGE, +} from '../constants'; +import { initialPanelsState, initialUiState } from '../store/state'; describe('SettingsMenu', () => { beforeEach(() => { @@ -31,144 +40,251 @@ describe('SettingsMenu', () => { }); }); - it('should render the flyout type button group', () => { - const flyoutCustomProps = { - hideSettings: false, - pushVsOverlay: { - disabled: false, - tooltip: '', - }, - }; - - const { getByTestId, queryByTestId } = render( - - - - ); - - getByTestId(SETTINGS_MENU_BUTTON_TEST_ID).click(); - - expect(getByTestId(SETTINGS_MENU_FLYOUT_TYPE_TITLE_TEST_ID)).toBeInTheDocument(); - expect( - queryByTestId(SETTINGS_MENU_FLYOUT_TYPE_INFORMATION_ICON_TEST_ID) - ).not.toBeInTheDocument(); - expect(getByTestId(SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_TEST_ID)).toBeInTheDocument(); - }); + describe('push vs overlay', () => { + it('should render the flyout type button group', () => { + const flyoutCustomProps = { + hideSettings: false, + pushVsOverlay: { + disabled: false, + tooltip: '', + }, + }; - it('should have the type selected if option is enabled', () => { - const state = { - panels: initialPanelsState, - ui: { - pushVsOverlay: 'push' as const, - }, - }; - const flyoutCustomProps = { - hideSettings: false, - pushVsOverlay: { - disabled: false, - tooltip: '', - }, - }; - - const { getByTestId } = render( - - - - ); - - getByTestId(SETTINGS_MENU_BUTTON_TEST_ID).click(); - - expect(getByTestId(SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_PUSH_TEST_ID)).toHaveClass( - 'euiButtonGroupButton-isSelected' - ); - expect(getByTestId(SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_OVERLAY_TEST_ID)).not.toHaveClass( - 'euiButtonGroupButton-isSelected' - ); - }); + const { getByTestId, queryByTestId } = render( + + + + ); + + getByTestId(SETTINGS_MENU_BUTTON_TEST_ID).click(); + + expect(getByTestId(SETTINGS_MENU_FLYOUT_TYPE_TITLE_TEST_ID)).toBeInTheDocument(); + expect( + queryByTestId(SETTINGS_MENU_FLYOUT_TYPE_INFORMATION_ICON_TEST_ID) + ).not.toBeInTheDocument(); + expect(getByTestId(SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_TEST_ID)).toBeInTheDocument(); + }); - it('should select correct the flyout type', () => { - const flyoutCustomProps = { - hideSettings: false, - pushVsOverlay: { - disabled: false, - tooltip: '', - }, - }; + it('should have the type selected if option is enabled', () => { + const state = { + panels: initialPanelsState, + ui: { + ...initialUiState, + pushVsOverlay: 'push' as const, + }, + }; + const flyoutCustomProps = { + hideSettings: false, + pushVsOverlay: { + disabled: false, + tooltip: '', + }, + }; - const { getByTestId } = render( - - - - ); + const { getByTestId } = render( + + + + ); - expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); + getByTestId(SETTINGS_MENU_BUTTON_TEST_ID).click(); - getByTestId(SETTINGS_MENU_BUTTON_TEST_ID).click(); - getByTestId(SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_PUSH_TEST_ID).click(); + expect(getByTestId(SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_PUSH_TEST_ID)).toHaveClass( + 'euiButtonGroupButton-isSelected' + ); + expect(getByTestId(SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_OVERLAY_TEST_ID)).not.toHaveClass( + 'euiButtonGroupButton-isSelected' + ); + }); - expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual( - JSON.stringify({ [PUSH_VS_OVERLAY_LOCAL_STORAGE]: 'push' }) - ); + it('should select correct the flyout type', () => { + const flyoutCustomProps = { + hideSettings: false, + pushVsOverlay: { + disabled: false, + tooltip: '', + }, + }; - getByTestId(SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_OVERLAY_TEST_ID).click(); + const { getByTestId } = render( + + + + ); - expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual( - JSON.stringify({ [PUSH_VS_OVERLAY_LOCAL_STORAGE]: 'overlay' }) - ); - }); + expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); - it('should render the the flyout type button group disabled', () => { - const flyoutCustomProps = { - hideSettings: false, - pushVsOverlay: { - disabled: true, - tooltip: 'This option is disabled', - }, - }; + getByTestId(SETTINGS_MENU_BUTTON_TEST_ID).click(); + getByTestId(SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_PUSH_TEST_ID).click(); - const { getByTestId } = render( - - - - ); + expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual( + JSON.stringify({ [PUSH_VS_OVERLAY_LOCAL_STORAGE]: 'push' }) + ); - expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); + getByTestId(SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_OVERLAY_TEST_ID).click(); + + expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual( + JSON.stringify({ [PUSH_VS_OVERLAY_LOCAL_STORAGE]: 'overlay' }) + ); + }); - getByTestId(SETTINGS_MENU_BUTTON_TEST_ID).click(); - expect(getByTestId(SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_TEST_ID)).toHaveAttribute('disabled'); + it('should render the the flyout type button group disabled', () => { + const flyoutCustomProps = { + hideSettings: false, + pushVsOverlay: { + disabled: true, + tooltip: 'This option is disabled', + }, + }; - expect(getByTestId(SETTINGS_MENU_FLYOUT_TYPE_INFORMATION_ICON_TEST_ID)).toBeInTheDocument(); + const { getByTestId } = render( + + + + ); - expect(getByTestId(SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_OVERLAY_TEST_ID)).toHaveClass( - 'euiButtonGroupButton-isSelected' - ); - expect(getByTestId(SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_PUSH_TEST_ID)).not.toHaveClass( - 'euiButtonGroupButton-isSelected' - ); + expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); - getByTestId(SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_PUSH_TEST_ID).click(); + getByTestId(SETTINGS_MENU_BUTTON_TEST_ID).click(); + expect(getByTestId(SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_TEST_ID)).toHaveAttribute( + 'disabled' + ); - expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); + expect(getByTestId(SETTINGS_MENU_FLYOUT_TYPE_INFORMATION_ICON_TEST_ID)).toBeInTheDocument(); + + expect(getByTestId(SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_OVERLAY_TEST_ID)).toHaveClass( + 'euiButtonGroupButton-isSelected' + ); + expect(getByTestId(SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_PUSH_TEST_ID)).not.toHaveClass( + 'euiButtonGroupButton-isSelected' + ); + + getByTestId(SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_PUSH_TEST_ID).click(); + + expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); + }); + + it('should not render the information icon if the tooltip is empty', () => { + const flyoutCustomProps = { + hideSettings: false, + pushVsOverlay: { + disabled: true, + tooltip: '', + }, + }; + + const { getByTestId, queryByTestId } = render( + + + + ); + + getByTestId(SETTINGS_MENU_BUTTON_TEST_ID).click(); + + expect( + queryByTestId(SETTINGS_MENU_FLYOUT_TYPE_INFORMATION_ICON_TEST_ID) + ).not.toBeInTheDocument(); + }); }); - it('should not render the information icon if the tooltip is empty', () => { - const flyoutCustomProps = { - hideSettings: false, - pushVsOverlay: { - disabled: true, - tooltip: '', - }, - }; - - const { getByTestId, queryByTestId } = render( - - - - ); - - getByTestId(SETTINGS_MENU_BUTTON_TEST_ID).click(); - expect( - queryByTestId(SETTINGS_MENU_FLYOUT_TYPE_INFORMATION_ICON_TEST_ID) - ).not.toBeInTheDocument(); + describe('resize', () => { + it('should render the flyout resize button', () => { + const flyoutCustomProps = { + hideSettings: false, + resize: { + disabled: false, + tooltip: '', + }, + }; + const { getByTestId, queryByTestId } = render( + + + + ); + + getByTestId(SETTINGS_MENU_BUTTON_TEST_ID).click(); + + expect(getByTestId(SETTINGS_MENU_FLYOUT_RESIZE_TITLE_TEST_ID)).toBeInTheDocument(); + expect( + queryByTestId(SETTINGS_MENU_FLYOUT_RESIZE_INFORMATION_ICON_TEST_ID) + ).not.toBeInTheDocument(); + expect(getByTestId(SETTINGS_MENU_FLYOUT_RESIZE_BUTTON_TEST_ID)).toBeInTheDocument(); + }); + + it('should reset correctly when clicked', () => { + const flyoutCustomProps = { + hideSettings: false, + resize: { + disabled: false, + tooltip: '', + }, + }; + + localStorage.setItem( + EXPANDABLE_FLYOUT_LOCAL_STORAGE, + JSON.stringify({ + [USER_COLLAPSED_WIDTH_LOCAL_STORAGE]: '250', + [USER_EXPANDED_WIDTH_LOCAL_STORAGE]: '500', + [USER_SECTION_WIDTHS_LOCAL_STORAGE]: { left: 50, right: 50 }, + }) + ); + + const { getByTestId } = render( + + + + ); + + getByTestId(SETTINGS_MENU_BUTTON_TEST_ID).click(); + getByTestId(SETTINGS_MENU_FLYOUT_RESIZE_BUTTON_TEST_ID).click(); + + const expandableFlyout = localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE); + expect(expandableFlyout).not.toBe(null); + + expect(expandableFlyout).not.toHaveProperty(USER_COLLAPSED_WIDTH_LOCAL_STORAGE); + expect(expandableFlyout).not.toHaveProperty(USER_EXPANDED_WIDTH_LOCAL_STORAGE); + expect(expandableFlyout).not.toHaveProperty(USER_SECTION_WIDTHS_LOCAL_STORAGE); + }); + + it('should render the the flyout resize button disabled', () => { + const flyoutCustomProps = { + hideSettings: false, + resize: { + disabled: true, + tooltip: 'This option is disabled', + }, + }; + + const { getByTestId } = render( + + + + ); + + getByTestId(SETTINGS_MENU_BUTTON_TEST_ID).click(); + expect(getByTestId(SETTINGS_MENU_FLYOUT_RESIZE_BUTTON_TEST_ID)).toHaveAttribute('disabled'); + expect(getByTestId(SETTINGS_MENU_FLYOUT_RESIZE_INFORMATION_ICON_TEST_ID)).toBeInTheDocument(); + }); + + it('should not render the information icon if the tooltip is empty', () => { + const flyoutCustomProps = { + hideSettings: false, + resize: { + disabled: true, + tooltip: '', + }, + }; + + const { getByTestId, queryByTestId } = render( + + + + ); + + getByTestId(SETTINGS_MENU_BUTTON_TEST_ID).click(); + expect( + queryByTestId(SETTINGS_MENU_FLYOUT_RESIZE_INFORMATION_ICON_TEST_ID) + ).not.toBeInTheDocument(); + }); }); }); diff --git a/packages/kbn-expandable-flyout/src/components/settings_menu.tsx b/packages/kbn-expandable-flyout/src/components/settings_menu.tsx index 7229921bfdd39..e632f8bb0172c 100644 --- a/packages/kbn-expandable-flyout/src/components/settings_menu.tsx +++ b/packages/kbn-expandable-flyout/src/components/settings_menu.tsx @@ -8,6 +8,7 @@ */ import { + EuiButtonEmpty, EuiButtonGroup, EuiButtonIcon, EuiContextMenu, @@ -22,9 +23,12 @@ import { import { css } from '@emotion/css'; import React, { memo, useCallback, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { changePushVsOverlayAction } from '../store/actions'; +import { changePushVsOverlayAction, resetAllUserChangedWidthsAction } from '../store/actions'; import { SETTINGS_MENU_BUTTON_TEST_ID, + SETTINGS_MENU_FLYOUT_RESIZE_BUTTON_TEST_ID, + SETTINGS_MENU_FLYOUT_RESIZE_INFORMATION_ICON_TEST_ID, + SETTINGS_MENU_FLYOUT_RESIZE_TITLE_TEST_ID, SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_OVERLAY_TEST_ID, SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_PUSH_TEST_ID, SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_TEST_ID, @@ -60,6 +64,12 @@ const FLYOUT_TYPE_OVERLAY_TOOLTIP = i18n.translate('expandableFlyout.settingsMen const FLYOUT_TYPE_PUSH_TOOLTIP = i18n.translate('expandableFlyout.settingsMenu.pushTooltip', { defaultMessage: 'Displays the flyout next to the page', }); +const FLYOUT_RESIZE_TITLE = i18n.translate('expandableFlyout.renderMenu.flyoutResizeTitle', { + defaultMessage: 'Flyout size', +}); +const FLYOUT_RESIZE_BUTTON = i18n.translate('expandableFlyout.renderMenu.flyoutResizeButton', { + defaultMessage: 'Reset size', +}); export interface FlyoutCustomProps { /** @@ -79,6 +89,19 @@ export interface FlyoutCustomProps { */ tooltip: string; }; + /** + * Control if the option to resize the flyout is enabled or not + */ + resize?: { + /** + * Disables the option + */ + disabled: boolean; + /** + * Tooltip to display + */ + tooltip: string; + }; } export interface SettingsMenuProps { @@ -119,6 +142,11 @@ export const SettingsMenu: React.FC = memo( [dispatch, flyoutCustomProps?.pushVsOverlay?.disabled] ); + const resetSizeOnClick = useCallback(() => { + dispatch(resetAllUserChangedWidthsAction()); + setPopover(false); + }, [dispatch]); + const panels = [ { id: 0, @@ -133,9 +161,6 @@ export const SettingsMenu: React.FC = memo( )} @@ -163,6 +188,28 @@ export const SettingsMenu: React.FC = memo( isDisabled={flyoutCustomProps?.pushVsOverlay?.disabled} data-test-subj={SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_TEST_ID} /> + + +

+ {FLYOUT_RESIZE_TITLE}{' '} + {flyoutCustomProps?.resize?.tooltip && ( + + + + )} +

+
+ + + {FLYOUT_RESIZE_BUTTON} + ), }, diff --git a/packages/kbn-expandable-flyout/src/components/test_ids.ts b/packages/kbn-expandable-flyout/src/components/test_ids.ts index 498342f1a227d..22d2e00ed66c7 100644 --- a/packages/kbn-expandable-flyout/src/components/test_ids.ts +++ b/packages/kbn-expandable-flyout/src/components/test_ids.ts @@ -7,6 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +export const FLYOUT_TEST_ID = 'resizableFlyout'; + export const RIGHT_SECTION_TEST_ID = 'rightSection'; export const LEFT_SECTION_TEST_ID = 'leftSection'; @@ -33,3 +35,16 @@ export const SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_OVERLAY_TEST_ID = export const SETTINGS_MENU_FLYOUT_TYPE_BUTTON_GROUP_PUSH_TEST_ID = 'settingsMenuFlyoutTypeButtonGroupPushOption'; + +export const SETTINGS_MENU_FLYOUT_RESIZE_TITLE_TEST_ID = 'settingsMenuFlyoutSizeTitle'; + +export const SETTINGS_MENU_FLYOUT_RESIZE_INFORMATION_ICON_TEST_ID = + 'settingsMenuFlyoutSizeInformationIcon'; + +export const SETTINGS_MENU_FLYOUT_RESIZE_BUTTON_TEST_ID = 'settingsMenuFlyoutSizeButton'; + +export const RESIZABLE_LEFT_SECTION_TEST_ID = 'resizableLeftSection'; + +export const RESIZABLE_RIGHT_SECTION_TEST_ID = 'resizableRightSection'; + +export const RESIZABLE_BUTTON_TEST_ID = 'resizableButton'; diff --git a/packages/kbn-expandable-flyout/src/constants.ts b/packages/kbn-expandable-flyout/src/constants.ts index 7ec81a9de4b67..dfabd845a3f20 100644 --- a/packages/kbn-expandable-flyout/src/constants.ts +++ b/packages/kbn-expandable-flyout/src/constants.ts @@ -14,3 +14,6 @@ export const REDUX_ID_FOR_MEMORY_STORAGE = 'memory'; export const EXPANDABLE_FLYOUT_LOCAL_STORAGE = 'expandableFlyout.ui'; export const PUSH_VS_OVERLAY_LOCAL_STORAGE = 'pushVsOverlay'; +export const USER_COLLAPSED_WIDTH_LOCAL_STORAGE = 'collapsedWidth'; +export const USER_EXPANDED_WIDTH_LOCAL_STORAGE = 'expandedWidth'; +export const USER_SECTION_WIDTHS_LOCAL_STORAGE = 'sectionWidths'; diff --git a/packages/kbn-expandable-flyout/src/hooks/use_expandable_flyout_state.ts b/packages/kbn-expandable-flyout/src/hooks/use_expandable_flyout_state.ts index 49cac7d97a895..88a94f66d54ae 100644 --- a/packages/kbn-expandable-flyout/src/hooks/use_expandable_flyout_state.ts +++ b/packages/kbn-expandable-flyout/src/hooks/use_expandable_flyout_state.ts @@ -7,6 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { useMemo } from 'react'; import { REDUX_ID_FOR_MEMORY_STORAGE } from '../constants'; import { useExpandableFlyoutContext } from '../context'; import { selectPanelsById, useSelector } from '../store/redux'; @@ -17,6 +18,7 @@ import { selectPanelsById, useSelector } from '../store/redux'; export const useExpandableFlyoutState = () => { const { urlKey } = useExpandableFlyoutContext(); // if no urlKey is provided, we are in memory storage mode and use the reserved word 'memory' - const id = urlKey || REDUX_ID_FOR_MEMORY_STORAGE; + const id = useMemo(() => urlKey || REDUX_ID_FOR_MEMORY_STORAGE, [urlKey]); + return useSelector(selectPanelsById(id)); }; diff --git a/packages/kbn-expandable-flyout/src/hooks/use_initialize_from_local_storage.test.ts b/packages/kbn-expandable-flyout/src/hooks/use_initialize_from_local_storage.test.ts index 70cc4f31f2636..26b3daf8161db 100644 --- a/packages/kbn-expandable-flyout/src/hooks/use_initialize_from_local_storage.test.ts +++ b/packages/kbn-expandable-flyout/src/hooks/use_initialize_from_local_storage.test.ts @@ -10,9 +10,20 @@ import { renderHook } from '@testing-library/react-hooks'; import { useInitializeFromLocalStorage } from './use_initialize_from_local_storage'; import { localStorageMock } from '../../__mocks__'; -import { EXPANDABLE_FLYOUT_LOCAL_STORAGE, PUSH_VS_OVERLAY_LOCAL_STORAGE } from '../constants'; +import { + USER_COLLAPSED_WIDTH_LOCAL_STORAGE, + EXPANDABLE_FLYOUT_LOCAL_STORAGE, + USER_EXPANDED_WIDTH_LOCAL_STORAGE, + USER_SECTION_WIDTHS_LOCAL_STORAGE, + PUSH_VS_OVERLAY_LOCAL_STORAGE, +} from '../constants'; import { useDispatch } from '../store/redux'; -import { changePushVsOverlayAction } from '../store/actions'; +import { + changeUserCollapsedWidthAction, + changeUserExpandedWidthAction, + changeUserSectionWidthsAction, + changePushVsOverlayAction, +} from '../store/actions'; jest.mock('../store/redux'); @@ -25,7 +36,7 @@ describe('useInitializeFromLocalStorage', () => { // if this test fails, it's very likely because the data format of the values saved in local storage // has changed and we might need to run a migration - it('should retrieve push/overlay value from local storage', () => { + it('should retrieve values from local storage', () => { const mockUseDispatch = jest.fn(); (useDispatch as jest.Mock).mockImplementation(() => mockUseDispatch); @@ -33,6 +44,9 @@ describe('useInitializeFromLocalStorage', () => { EXPANDABLE_FLYOUT_LOCAL_STORAGE, JSON.stringify({ [PUSH_VS_OVERLAY_LOCAL_STORAGE]: 'push', + [USER_COLLAPSED_WIDTH_LOCAL_STORAGE]: 250, + [USER_EXPANDED_WIDTH_LOCAL_STORAGE]: 500, + [USER_SECTION_WIDTHS_LOCAL_STORAGE]: { left: 50, right: 50 }, }) ); @@ -44,6 +58,25 @@ describe('useInitializeFromLocalStorage', () => { savedToLocalStorage: false, }) ); + expect(mockUseDispatch).toHaveBeenCalledWith( + changeUserCollapsedWidthAction({ + width: 250, + savedToLocalStorage: false, + }) + ); + expect(mockUseDispatch).toHaveBeenCalledWith( + changeUserExpandedWidthAction({ + width: 500, + savedToLocalStorage: false, + }) + ); + expect(mockUseDispatch).toHaveBeenCalledWith( + changeUserSectionWidthsAction({ + right: 50, + left: 50, + savedToLocalStorage: false, + }) + ); }); it('should not dispatch action if expandable flyout key is not present in local storage', () => { @@ -51,9 +84,12 @@ describe('useInitializeFromLocalStorage', () => { (useDispatch as jest.Mock).mockImplementation(() => mockUseDispatch); localStorage.setItem( - EXPANDABLE_FLYOUT_LOCAL_STORAGE, + 'wrong_top_level_key', JSON.stringify({ - wrong_key: 'push', + [PUSH_VS_OVERLAY_LOCAL_STORAGE]: 'push', + [USER_COLLAPSED_WIDTH_LOCAL_STORAGE]: 250, + [USER_EXPANDED_WIDTH_LOCAL_STORAGE]: 500, + [USER_SECTION_WIDTHS_LOCAL_STORAGE]: { left: 50, right: 50 }, }) ); @@ -62,10 +98,17 @@ describe('useInitializeFromLocalStorage', () => { expect(mockUseDispatch).not.toHaveBeenCalled(); }); - it('should not dispatch action if expandable flyout key is present in local storage but not push/overlay', () => { + it('should not dispatch action if expandable flyout key is present in local storage but no has no properties', () => { const mockUseDispatch = jest.fn(); (useDispatch as jest.Mock).mockImplementation(() => mockUseDispatch); + localStorage.setItem( + EXPANDABLE_FLYOUT_LOCAL_STORAGE, + JSON.stringify({ + wrong_key: 'push', + }) + ); + renderHook(() => useInitializeFromLocalStorage()); expect(mockUseDispatch).not.toHaveBeenCalled(); diff --git a/packages/kbn-expandable-flyout/src/hooks/use_initialize_from_local_storage.ts b/packages/kbn-expandable-flyout/src/hooks/use_initialize_from_local_storage.ts index 7af92a726a394..9c88fe29e75d7 100644 --- a/packages/kbn-expandable-flyout/src/hooks/use_initialize_from_local_storage.ts +++ b/packages/kbn-expandable-flyout/src/hooks/use_initialize_from_local_storage.ts @@ -7,12 +7,27 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { EXPANDABLE_FLYOUT_LOCAL_STORAGE, PUSH_VS_OVERLAY_LOCAL_STORAGE } from '../constants'; +import { + USER_COLLAPSED_WIDTH_LOCAL_STORAGE, + EXPANDABLE_FLYOUT_LOCAL_STORAGE, + USER_EXPANDED_WIDTH_LOCAL_STORAGE, + USER_SECTION_WIDTHS_LOCAL_STORAGE, + PUSH_VS_OVERLAY_LOCAL_STORAGE, +} from '../constants'; import { useDispatch } from '../store/redux'; -import { changePushVsOverlayAction } from '../store/actions'; +import { + changeUserCollapsedWidthAction, + changeUserExpandedWidthAction, + changeUserSectionWidthsAction, + changePushVsOverlayAction, +} from '../store/actions'; /** - * Hook to initialize the push vs overlay redux state from local storage + * Hook to initialize all the values in redux state from local storage + * - push vs overlay + * - user's custom collapsed width + * - user's custom expanded width + * - user's custom section percentages */ export const useInitializeFromLocalStorage = () => { const dispatch = useDispatch(); @@ -29,4 +44,35 @@ export const useInitializeFromLocalStorage = () => { }) ); } + + const userCollapsedFlyoutWidth = JSON.parse(expandableFlyout)[USER_COLLAPSED_WIDTH_LOCAL_STORAGE]; + if (userCollapsedFlyoutWidth) { + dispatch( + changeUserCollapsedWidthAction({ + width: parseInt(userCollapsedFlyoutWidth, 10), + savedToLocalStorage: false, + }) + ); + } + + const userExpandedFlyoutWidth = JSON.parse(expandableFlyout)[USER_EXPANDED_WIDTH_LOCAL_STORAGE]; + if (userExpandedFlyoutWidth) { + dispatch( + changeUserExpandedWidthAction({ + width: parseInt(userExpandedFlyoutWidth, 10), + savedToLocalStorage: false, + }) + ); + } + + const userSectionWidths = JSON.parse(expandableFlyout)[USER_SECTION_WIDTHS_LOCAL_STORAGE]; + if (userSectionWidths) { + dispatch( + changeUserSectionWidthsAction({ + right: userSectionWidths.right, + left: userSectionWidths.left, + savedToLocalStorage: false, + }) + ); + } }; diff --git a/packages/kbn-expandable-flyout/src/hooks/use_sections.test.tsx b/packages/kbn-expandable-flyout/src/hooks/use_sections.test.tsx new file mode 100644 index 0000000000000..4526f128affd3 --- /dev/null +++ b/packages/kbn-expandable-flyout/src/hooks/use_sections.test.tsx @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import type { RenderHookResult } from '@testing-library/react-hooks'; +import type { UseSectionsParams, UseSectionsResult } from './use_sections'; +import { useSections } from './use_sections'; +import { useExpandableFlyoutState } from '../..'; + +jest.mock('../..'); + +describe('useSections', () => { + let hookResult: RenderHookResult; + + it('should return undefined for all values if no registeredPanels', () => { + (useExpandableFlyoutState as jest.Mock).mockReturnValue({ + left: undefined, + right: undefined, + preview: undefined, + }); + + const initialProps: UseSectionsParams = { + registeredPanels: [], + }; + hookResult = renderHook((props: UseSectionsParams) => useSections(props), { + initialProps, + }); + + expect(hookResult.result.current).toEqual({ + leftSection: undefined, + rightSection: undefined, + previewSection: undefined, + mostRecentPreviewBanner: undefined, + mostRecentPreview: undefined, + }); + }); + + it('should return all sections', () => { + (useExpandableFlyoutState as jest.Mock).mockReturnValue({ + left: { id: 'left' }, + right: { id: 'right' }, + preview: [{ id: 'preview' }], + }); + + const initialProps: UseSectionsParams = { + registeredPanels: [ + { + key: 'right', + component: () =>
{'component'}
, + }, + { + key: 'left', + component: () =>
{'component'}
, + }, + { + key: 'preview', + component: () =>
{'component'}
, + }, + ], + }; + hookResult = renderHook((props: UseSectionsParams) => useSections(props), { + initialProps, + }); + + expect(hookResult.result.current.rightSection?.key).toEqual('right'); + expect(hookResult.result.current.rightSection?.component).toBeDefined(); + + expect(hookResult.result.current.leftSection?.key).toEqual('left'); + expect(hookResult.result.current.leftSection?.component).toBeDefined(); + + expect(hookResult.result.current.previewSection?.key).toEqual('preview'); + expect(hookResult.result.current.previewSection?.component).toBeDefined(); + + expect(hookResult.result.current.mostRecentPreviewBanner).toEqual(undefined); + expect(hookResult.result.current.mostRecentPreview).toEqual({ id: 'preview' }); + }); + + it('should return preview banner', () => { + (useExpandableFlyoutState as jest.Mock).mockReturnValue({ + preview: [ + { + id: 'preview', + params: { + banner: { + title: 'title', + backgroundColor: 'primary', + textColor: 'red', + }, + }, + }, + ], + }); + + const initialProps: UseSectionsParams = { + registeredPanels: [ + { + key: 'preview', + component: () =>
{'component'}
, + }, + ], + }; + hookResult = renderHook((props: UseSectionsParams) => useSections(props), { + initialProps, + }); + + expect(hookResult.result.current.mostRecentPreviewBanner).toEqual({ + title: 'title', + backgroundColor: 'primary', + textColor: 'red', + }); + }); + + it('should return most recent preview', () => { + (useExpandableFlyoutState as jest.Mock).mockReturnValue({ + preview: [{ id: 'preview1' }, { id: 'preview2' }, { id: 'preview3' }], + }); + + const initialProps: UseSectionsParams = { + registeredPanels: [], + }; + hookResult = renderHook((props: UseSectionsParams) => useSections(props), { + initialProps, + }); + + expect(hookResult.result.current.mostRecentPreview).toEqual({ id: 'preview3' }); + }); +}); diff --git a/packages/kbn-expandable-flyout/src/hooks/use_sections.ts b/packages/kbn-expandable-flyout/src/hooks/use_sections.ts new file mode 100644 index 0000000000000..5267030790e38 --- /dev/null +++ b/packages/kbn-expandable-flyout/src/hooks/use_sections.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { useMemo } from 'react'; +import { isPreviewBanner, PreviewBanner } from '../components/preview_section'; +import { FlyoutPanelProps, useExpandableFlyoutState } from '../..'; +import { Panel } from '../types'; + +export interface UseSectionsParams { + /** + * List of all registered panels available for render + */ + registeredPanels: Panel[]; +} + +export interface UseSectionsResult { + /** + * The left section to be displayed in the flyout. + */ + leftSection: Panel | undefined; + /** + * The right section to be displayed in the flyout. + */ + rightSection: Panel | undefined; + /** + * The preview section to be displayed in the flyout. + */ + previewSection: Panel | undefined; + /** + * The most recent preview information to be displayed in the preview section. + */ + mostRecentPreview: FlyoutPanelProps | undefined; + /** + * The preview banner to be displayed in preview section. + */ + mostRecentPreviewBanner: PreviewBanner | undefined; +} + +/** + * Hook that retrieves the left, right, and preview sections to be displayed in the flyout. + */ +export const useSections = ({ registeredPanels }: UseSectionsParams): UseSectionsResult => { + const { left, preview, right } = useExpandableFlyoutState(); + + const rightSection = useMemo( + () => registeredPanels.find((panel) => panel.key === right?.id), + [right, registeredPanels] + ); + const leftSection = useMemo( + () => registeredPanels.find((panel) => panel.key === left?.id), + [left, registeredPanels] + ); + // retrieve the last preview panel (most recent) + const mostRecentPreview = useMemo( + () => (preview ? preview[preview.length - 1] : undefined), + [preview] + ); + const previewSection = useMemo( + () => registeredPanels.find((panel) => panel.key === mostRecentPreview?.id), + [mostRecentPreview, registeredPanels] + ); + const mostRecentPreviewBanner = useMemo( + () => + isPreviewBanner(mostRecentPreview?.params?.banner) + ? mostRecentPreview?.params?.banner + : undefined, + [mostRecentPreview?.params?.banner] + ); + + return useMemo( + () => ({ + leftSection, + rightSection, + previewSection, + mostRecentPreviewBanner, + mostRecentPreview, + }), + [leftSection, rightSection, previewSection, mostRecentPreviewBanner, mostRecentPreview] + ); +}; diff --git a/packages/kbn-expandable-flyout/src/hooks/use_sections_sizes.test.ts b/packages/kbn-expandable-flyout/src/hooks/use_sections_sizes.test.ts deleted file mode 100644 index f1e9c4e3bf072..0000000000000 --- a/packages/kbn-expandable-flyout/src/hooks/use_sections_sizes.test.ts +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { renderHook } from '@testing-library/react-hooks'; -import type { RenderHookResult } from '@testing-library/react-hooks'; -import type { UserSectionsSizesParams, UserSectionsSizesResult } from './use_sections_sizes'; -import { useSectionSizes } from './use_sections_sizes'; - -describe('useSectionSizes', () => { - let hookResult: RenderHookResult; - - describe('Right section', () => { - it('should return 0 for right section if it is hidden', () => { - const initialProps = { - windowWidth: 350, - showRight: false, - showLeft: false, - showPreview: false, - }; - hookResult = renderHook((props: UserSectionsSizesParams) => useSectionSizes(props), { - initialProps, - }); - - expect(hookResult.result.current).toEqual({ - rightSectionWidth: 0, - leftSectionWidth: 0, - flyoutWidth: '0px', - previewSectionLeft: 0, - }); - }); - - it('should return the window width for right section size for tiny screen', () => { - const initialProps = { - windowWidth: 350, - showRight: true, - showLeft: false, - showPreview: false, - }; - hookResult = renderHook((props: UserSectionsSizesParams) => useSectionSizes(props), { - initialProps, - }); - - expect(hookResult.result.current).toEqual({ - rightSectionWidth: 350, - leftSectionWidth: 0, - flyoutWidth: '350px', - previewSectionLeft: 0, - }); - }); - - it('should return 380 for right section size for medium screen', () => { - const initialProps = { - windowWidth: 600, - showRight: true, - showLeft: false, - showPreview: false, - }; - hookResult = renderHook((props: UserSectionsSizesParams) => useSectionSizes(props), { - initialProps, - }); - - expect(hookResult.result.current).toEqual({ - rightSectionWidth: 380, - leftSectionWidth: 0, - flyoutWidth: '380px', - previewSectionLeft: 0, - }); - }); - - it('should return 500 for right section size for large screen', () => { - const initialProps = { - windowWidth: 1300, - showRight: true, - showLeft: false, - showPreview: false, - }; - hookResult = renderHook((props: UserSectionsSizesParams) => useSectionSizes(props), { - initialProps, - }); - - expect(hookResult.result.current.rightSectionWidth).toBeGreaterThan(420); - expect(hookResult.result.current.rightSectionWidth).toBeLessThan(750); - expect(hookResult.result.current.leftSectionWidth).toEqual(0); - expect(hookResult.result.current.flyoutWidth).toEqual( - `${hookResult.result.current.rightSectionWidth}px` - ); - expect(hookResult.result.current.previewSectionLeft).toEqual(0); - }); - - it('should return 750 for right section size for very large screen', () => { - const initialProps = { - windowWidth: 2500, - showRight: true, - showLeft: false, - showPreview: false, - }; - hookResult = renderHook((props: UserSectionsSizesParams) => useSectionSizes(props), { - initialProps, - }); - - expect(hookResult.result.current).toEqual({ - rightSectionWidth: 750, - leftSectionWidth: 0, - flyoutWidth: '750px', - previewSectionLeft: 0, - }); - }); - }); - - describe('Left section', () => { - it('should return 0 for left section if it is hidden', () => { - const initialProps = { - windowWidth: 500, - showRight: true, - showLeft: false, - showPreview: false, - }; - hookResult = renderHook((props: UserSectionsSizesParams) => useSectionSizes(props), { - initialProps, - }); - - expect(hookResult.result.current).toEqual({ - rightSectionWidth: 380, - leftSectionWidth: 0, - flyoutWidth: '380px', - previewSectionLeft: 0, - }); - }); - - it('should return the remaining for left section', () => { - const initialProps = { - windowWidth: 500, - showRight: true, - showLeft: true, - showPreview: false, - }; - hookResult = renderHook((props: UserSectionsSizesParams) => useSectionSizes(props), { - initialProps, - }); - - expect(hookResult.result.current).toEqual({ - rightSectionWidth: 380, - leftSectionWidth: 72, - flyoutWidth: '452px', - previewSectionLeft: 0, - }); - }); - - it('should return 80% of remaining for left section', () => { - const initialProps = { - windowWidth: 2500, - showRight: true, - showLeft: true, - showPreview: false, - }; - hookResult = renderHook((props: UserSectionsSizesParams) => useSectionSizes(props), { - initialProps, - }); - - expect(hookResult.result.current.rightSectionWidth).toEqual(750); - expect(hookResult.result.current.leftSectionWidth).toEqual((2500 - 750) * 0.8); - expect(hookResult.result.current.flyoutWidth).toEqual( - `${ - hookResult.result.current.rightSectionWidth + hookResult.result.current.leftSectionWidth - }px` - ); - expect(hookResult.result.current.previewSectionLeft).toEqual(0); - }); - - it('should return max out at 1500px for really big screens', () => { - const initialProps = { - windowWidth: 2700, - showRight: true, - showLeft: true, - showPreview: false, - }; - hookResult = renderHook((props: UserSectionsSizesParams) => useSectionSizes(props), { - initialProps, - }); - - expect(hookResult.result.current.rightSectionWidth).toEqual(750); - expect(hookResult.result.current.leftSectionWidth).toEqual(1500); - expect(hookResult.result.current.flyoutWidth).toEqual( - `${ - hookResult.result.current.rightSectionWidth + hookResult.result.current.leftSectionWidth - }px` - ); - expect(hookResult.result.current.previewSectionLeft).toEqual(0); - }); - }); - - describe('Preview section', () => { - it('should return the 0 for preview section if it is hidden', () => { - const initialProps = { - windowWidth: 600, - showRight: true, - showLeft: false, - showPreview: false, - }; - hookResult = renderHook((props: UserSectionsSizesParams) => useSectionSizes(props), { - initialProps, - }); - - expect(hookResult.result.current).toEqual({ - rightSectionWidth: 380, - leftSectionWidth: 0, - flyoutWidth: '380px', - previewSectionLeft: 0, - }); - }); - - it('should return the 0 for preview section when left section is hidden', () => { - const initialProps = { - windowWidth: 600, - showRight: true, - showLeft: false, - showPreview: true, - }; - hookResult = renderHook((props: UserSectionsSizesParams) => useSectionSizes(props), { - initialProps, - }); - - expect(hookResult.result.current).toEqual({ - rightSectionWidth: 380, - leftSectionWidth: 0, - flyoutWidth: '380px', - previewSectionLeft: 0, - }); - }); - - it('should return for preview section when left section is visible', () => { - const initialProps = { windowWidth: 600, showRight: true, showLeft: true, showPreview: true }; - hookResult = renderHook((props: UserSectionsSizesParams) => useSectionSizes(props), { - initialProps, - }); - - expect(hookResult.result.current).toEqual({ - rightSectionWidth: 380, - leftSectionWidth: 172, - flyoutWidth: '552px', - previewSectionLeft: 172, - }); - }); - }); -}); diff --git a/packages/kbn-expandable-flyout/src/hooks/use_sections_sizes.ts b/packages/kbn-expandable-flyout/src/hooks/use_sections_sizes.ts deleted file mode 100644 index b255010b06967..0000000000000 --- a/packages/kbn-expandable-flyout/src/hooks/use_sections_sizes.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -const RIGHT_SECTION_MIN_WIDTH = 380; -const MIN_RESOLUTION_BREAKPOINT = 992; -const RIGHT_SECTION_MAX_WIDTH = 750; -const MAX_RESOLUTION_BREAKPOINT = 1920; - -const LEFT_SECTION_MAX_WIDTH = 1500; - -const FULL_WIDTH_BREAKPOINT = 1600; -const FULL_WIDTH_PADDING = 48; - -export interface UserSectionsSizesParams { - /** - * The width of the browser window - */ - windowWidth: number; - /** - * True if the right section is visible, false otherwise - */ - showRight: boolean; - /** - * True if the left section is visible, false otherwise - */ - showLeft: boolean; - /** - * True if the preview section is visible, false otherwise - */ - showPreview: boolean; -} - -export interface UserSectionsSizesResult { - /** - * Width of the right section in pixels - */ - rightSectionWidth: number; - /** - * Width of the left section in pixels - */ - leftSectionWidth: number; - /** - * Width of the flyout in pixels - */ - flyoutWidth: string; - /** - * Left position of the preview section in pixels - */ - previewSectionLeft: number; -} - -/** - * Hook that calculate the different width for the sections of the flyout and the flyout itself - */ -export const useSectionSizes = ({ - windowWidth, - showRight, - showLeft, - showPreview, -}: UserSectionsSizesParams): UserSectionsSizesResult => { - let rightSectionWidth: number = 0; - if (showRight) { - if (windowWidth < MIN_RESOLUTION_BREAKPOINT) { - // the right section's width will grow from 380px (at 992px resolution) while handling tiny screens by not going smaller than the window width - rightSectionWidth = Math.min(RIGHT_SECTION_MIN_WIDTH, windowWidth); - } else { - const ratioWidth = - (RIGHT_SECTION_MAX_WIDTH - RIGHT_SECTION_MIN_WIDTH) * - ((windowWidth - MIN_RESOLUTION_BREAKPOINT) / - (MAX_RESOLUTION_BREAKPOINT - MIN_RESOLUTION_BREAKPOINT)); - - // the right section's width will grow to 750px (at 1920px resolution) and will never go bigger than 750px in higher resolutions - rightSectionWidth = Math.min(RIGHT_SECTION_MIN_WIDTH + ratioWidth, RIGHT_SECTION_MAX_WIDTH); - } - } - - let leftSectionWidth: number = 0; - if (showLeft) { - // the left section's width will be nearly the remaining space for resolution lower than 1600px - if (windowWidth <= FULL_WIDTH_BREAKPOINT) { - leftSectionWidth = windowWidth - rightSectionWidth - FULL_WIDTH_PADDING; - } else { - // the left section's width will be taking 80% of the remaining space for resolution higher than 1600px, while never going bigger than 1500px - leftSectionWidth = Math.min( - ((windowWidth - rightSectionWidth) * 80) / 100, - LEFT_SECTION_MAX_WIDTH - ); - } - } - - const flyoutWidth: string = - showRight && showLeft ? `${rightSectionWidth + leftSectionWidth}px` : `${rightSectionWidth}px`; - - // preview section's width should only be similar to the right section. - // Though because the preview is rendered with an absolute position in the flyout, we calculate its left position instead of the width - let previewSectionLeft: number = 0; - if (showPreview) { - // the preview section starts where the left section ends - previewSectionLeft = leftSectionWidth; - } - - return { - rightSectionWidth, - leftSectionWidth, - flyoutWidth, - previewSectionLeft, - }; -}; diff --git a/packages/kbn-expandable-flyout/src/hooks/use_window_size.test.ts b/packages/kbn-expandable-flyout/src/hooks/use_window_size.test.ts deleted file mode 100644 index e53268466497d..0000000000000 --- a/packages/kbn-expandable-flyout/src/hooks/use_window_size.test.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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { renderHook } from '@testing-library/react-hooks'; -import { useWindowSize } from './use_window_size'; - -describe('useWindowSize', () => { - it('should return the window size', () => { - const hookResult = renderHook(() => useWindowSize()); - expect(hookResult.result.current).toEqual(1024); - }); -}); diff --git a/packages/kbn-expandable-flyout/src/hooks/use_window_size.ts b/packages/kbn-expandable-flyout/src/hooks/use_window_size.ts deleted file mode 100644 index 268e70b8f6d6c..0000000000000 --- a/packages/kbn-expandable-flyout/src/hooks/use_window_size.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { useLayoutEffect, useState } from 'react'; - -/** - * Hook that returns the browser window width - */ -export const useWindowSize = (): number => { - const [width, setWidth] = useState(0); - useLayoutEffect(() => { - function updateSize() { - setWidth(window.innerWidth); - } - window.addEventListener('resize', updateSize); - updateSize(); - return () => window.removeEventListener('resize', updateSize); - }, []); - return width; -}; diff --git a/packages/kbn-expandable-flyout/src/hooks/use_window_width.test.ts b/packages/kbn-expandable-flyout/src/hooks/use_window_width.test.ts new file mode 100644 index 0000000000000..72ab9148743db --- /dev/null +++ b/packages/kbn-expandable-flyout/src/hooks/use_window_width.test.ts @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { + FULL_WIDTH_PADDING, + MAX_RESOLUTION_BREAKPOINT, + MIN_RESOLUTION_BREAKPOINT, + RIGHT_SECTION_MAX_WIDTH, + RIGHT_SECTION_MIN_WIDTH, + useWindowWidth, +} from './use_window_width'; +import { useDispatch } from '../store/redux'; +import { setDefaultWidthsAction } from '../store/actions'; + +jest.mock('../store/redux'); + +describe('useWindowWidth', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should return the window size and dispatch setDefaultWidthsAction', () => { + global.innerWidth = 1024; + + const mockUseDispatch = jest.fn(); + (useDispatch as jest.Mock).mockImplementation(() => mockUseDispatch); + + const hookResult = renderHook(() => useWindowWidth()); + + expect(hookResult.result.current).toEqual(1024); + expect(mockUseDispatch).toHaveBeenCalled(); + }); + + it('should not dispatch action if window.innerWidth is 0', () => { + global.innerWidth = 0; + + const mockUseDispatch = jest.fn(); + (useDispatch as jest.Mock).mockImplementation(() => mockUseDispatch); + + const hookResult = renderHook(() => useWindowWidth()); + + expect(hookResult.result.current).toEqual(0); + expect(mockUseDispatch).not.toHaveBeenCalled(); + }); + + it('should handle very small screens', () => { + global.innerWidth = 300; + + const mockUseDispatch = jest.fn(); + (useDispatch as jest.Mock).mockImplementation(() => mockUseDispatch); + + const hookResult = renderHook(() => useWindowWidth()); + + expect(hookResult.result.current).toEqual(300); + expect(mockUseDispatch).toHaveBeenCalledWith( + setDefaultWidthsAction({ + left: -48, + right: 300, + preview: 300, + }) + ); + }); + + it('should handle small screens', () => { + global.innerWidth = 500; + + const mockUseDispatch = jest.fn(); + (useDispatch as jest.Mock).mockImplementation(() => mockUseDispatch); + + const hookResult = renderHook(() => useWindowWidth()); + + expect(hookResult.result.current).toEqual(500); + expect(mockUseDispatch).toHaveBeenCalledWith( + setDefaultWidthsAction({ + left: 72, + right: 380, + preview: 380, + }) + ); + }); + + it('should handle medium screens', () => { + global.innerWidth = 1300; + + const mockUseDispatch = jest.fn(); + (useDispatch as jest.Mock).mockImplementation(() => mockUseDispatch); + + const hookResult = renderHook(() => useWindowWidth()); + + const right = + RIGHT_SECTION_MIN_WIDTH + + (RIGHT_SECTION_MAX_WIDTH - RIGHT_SECTION_MIN_WIDTH) * + ((1300 - MIN_RESOLUTION_BREAKPOINT) / + (MAX_RESOLUTION_BREAKPOINT - MIN_RESOLUTION_BREAKPOINT)); + const left = 1300 - right - FULL_WIDTH_PADDING; + const preview = right; + + expect(hookResult.result.current).toEqual(1300); + expect(mockUseDispatch).toHaveBeenCalledWith( + setDefaultWidthsAction({ + left, + right, + preview, + }) + ); + }); + + it('should handle large screens', () => { + global.innerWidth = 2500; + + const mockUseDispatch = jest.fn(); + (useDispatch as jest.Mock).mockImplementation(() => mockUseDispatch); + + const hookResult = renderHook(() => useWindowWidth()); + + expect(hookResult.result.current).toEqual(2500); + expect(mockUseDispatch).toHaveBeenCalledWith( + setDefaultWidthsAction({ + left: 1400, + right: 750, + preview: 750, + }) + ); + }); + + it('should handle very large screens', () => { + global.innerWidth = 3800; + + const mockUseDispatch = jest.fn(); + (useDispatch as jest.Mock).mockImplementation(() => mockUseDispatch); + + const hookResult = renderHook(() => useWindowWidth()); + + expect(hookResult.result.current).toEqual(3800); + expect(mockUseDispatch).toHaveBeenCalledWith( + setDefaultWidthsAction({ + left: 1500, + right: 750, + preview: 750, + }) + ); + }); +}); diff --git a/packages/kbn-expandable-flyout/src/hooks/use_window_width.ts b/packages/kbn-expandable-flyout/src/hooks/use_window_width.ts new file mode 100644 index 0000000000000..3df9eef08a4f8 --- /dev/null +++ b/packages/kbn-expandable-flyout/src/hooks/use_window_width.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { useLayoutEffect, useState } from 'react'; +import { useDispatch } from '../store/redux'; +import { setDefaultWidthsAction } from '../store/actions'; + +export const RIGHT_SECTION_MIN_WIDTH = 380; +export const MIN_RESOLUTION_BREAKPOINT = 992; +export const RIGHT_SECTION_MAX_WIDTH = 750; +export const MAX_RESOLUTION_BREAKPOINT = 1920; + +const LEFT_SECTION_MAX_WIDTH = 1500; + +const FULL_WIDTH_BREAKPOINT = 1600; +export const FULL_WIDTH_PADDING = 48; + +/** + * Hook that returns the browser window width + */ +export const useWindowWidth = (): number => { + const dispatch = useDispatch(); + + const [width, setWidth] = useState(0); + + useLayoutEffect(() => { + function updateSize() { + setWidth(window.innerWidth); + + const windowWidth = window.innerWidth; + if (windowWidth !== 0) { + let rightSectionWidth: number; + if (windowWidth < MIN_RESOLUTION_BREAKPOINT) { + // the right section's width will grow from 380px (at 992px resolution) while handling tiny screens by not going smaller than the window width + rightSectionWidth = Math.min(RIGHT_SECTION_MIN_WIDTH, windowWidth); + } else { + const ratioWidth = + (RIGHT_SECTION_MAX_WIDTH - RIGHT_SECTION_MIN_WIDTH) * + ((windowWidth - MIN_RESOLUTION_BREAKPOINT) / + (MAX_RESOLUTION_BREAKPOINT - MIN_RESOLUTION_BREAKPOINT)); + + // the right section's width will grow to 750px (at 1920px resolution) and will never go bigger than 750px in higher resolutions + rightSectionWidth = Math.min( + RIGHT_SECTION_MIN_WIDTH + ratioWidth, + RIGHT_SECTION_MAX_WIDTH + ); + } + + let leftSectionWidth: number; + // the left section's width will be nearly the remaining space for resolution lower than 1600px + if (windowWidth <= FULL_WIDTH_BREAKPOINT) { + leftSectionWidth = windowWidth - rightSectionWidth - FULL_WIDTH_PADDING; + } else { + // the left section's width will be taking 80% of the remaining space for resolution higher than 1600px, while never going bigger than 1500px + leftSectionWidth = Math.min( + ((windowWidth - rightSectionWidth) * 80) / 100, + LEFT_SECTION_MAX_WIDTH + ); + } + + const previewSectionWidth: number = rightSectionWidth; + + dispatch( + setDefaultWidthsAction({ + right: rightSectionWidth, + left: leftSectionWidth, + preview: previewSectionWidth, + }) + ); + } + } + window.addEventListener('resize', updateSize); + updateSize(); + return () => window.removeEventListener('resize', updateSize); + }, [dispatch]); + + return width; +}; diff --git a/packages/kbn-expandable-flyout/src/index.stories.tsx b/packages/kbn-expandable-flyout/src/index.stories.tsx index 6e6e7207d8f15..1e8e08d96c073 100644 --- a/packages/kbn-expandable-flyout/src/index.stories.tsx +++ b/packages/kbn-expandable-flyout/src/index.stories.tsx @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { ExpandableFlyout } from '.'; import { TestProvider } from './test/provider'; -import { State } from './store/state'; +import { initialUiState, State } from './store/state'; export default { component: ExpandableFlyout, @@ -114,9 +114,7 @@ export const Right: Story = () => { }, }, }, - ui: { - pushVsOverlay: 'overlay', - }, + ui: initialUiState, }; return ( @@ -144,9 +142,7 @@ export const Left: Story = () => { }, }, }, - ui: { - pushVsOverlay: 'overlay', - }, + ui: initialUiState, }; return ( @@ -178,9 +174,7 @@ export const Preview: Story = () => { }, }, }, - ui: { - pushVsOverlay: 'overlay', - }, + ui: initialUiState, }; return ( @@ -215,9 +209,7 @@ export const MultiplePreviews: Story = () => { }, }, }, - ui: { - pushVsOverlay: 'overlay', - }, + ui: initialUiState, }; return ( @@ -230,7 +222,7 @@ export const MultiplePreviews: Story = () => { ); }; -export const CollapsedPushVsOverlay: Story = () => { +export const CollapsedPushMode: Story = () => { const state: State = { panels: { byId: { @@ -244,6 +236,7 @@ export const CollapsedPushVsOverlay: Story = () => { }, }, ui: { + ...initialUiState, pushVsOverlay: 'push', }, }; @@ -255,7 +248,7 @@ export const CollapsedPushVsOverlay: Story = () => { ); }; -export const ExpandedPushVsOverlay: Story = () => { +export const ExpandedPushMode: Story = () => { const state: State = { panels: { byId: { @@ -271,6 +264,7 @@ export const ExpandedPushVsOverlay: Story = () => { }, }, ui: { + ...initialUiState, pushVsOverlay: 'push', }, }; @@ -297,9 +291,7 @@ export const DisableTypeSelection: Story = () => { }, }, }, - ui: { - pushVsOverlay: 'overlay', - }, + ui: initialUiState, }; return ( @@ -313,3 +305,58 @@ export const DisableTypeSelection: Story = () => { ); }; + +export const ResetWidths: Story = () => { + const state: State = { + panels: { + byId: { + memory: { + right: { + id: 'right', + }, + left: { + id: 'left', + }, + preview: undefined, + }, + }, + }, + ui: initialUiState, + }; + + return ( + + + + ); +}; + +export const DisableResizeWidthSelection: Story = () => { + const state: State = { + panels: { + byId: { + memory: { + right: { + id: 'right', + }, + left: { + id: 'left', + }, + preview: undefined, + }, + }, + }, + ui: initialUiState, + }; + + return ( + + + + ); +}; diff --git a/packages/kbn-expandable-flyout/src/index.test.tsx b/packages/kbn-expandable-flyout/src/index.test.tsx index f465a16501761..8ee4ff32a9821 100644 --- a/packages/kbn-expandable-flyout/src/index.test.tsx +++ b/packages/kbn-expandable-flyout/src/index.test.tsx @@ -12,17 +12,13 @@ import { render } from '@testing-library/react'; import { Panel } from './types'; import { ExpandableFlyout } from '.'; -import { - LEFT_SECTION_TEST_ID, - PREVIEW_SECTION_TEST_ID, - SETTINGS_MENU_BUTTON_TEST_ID, - RIGHT_SECTION_TEST_ID, -} from './components/test_ids'; -import { type State } from './store/state'; +import { useWindowWidth } from './hooks/use_window_width'; import { TestProvider } from './test/provider'; import { REDUX_ID_FOR_MEMORY_STORAGE } from './constants'; +import { initialUiState } from './store/state'; + +jest.mock('./hooks/use_window_width'); -const id = REDUX_ID_FOR_MEMORY_STORAGE; const registeredPanels: Panel[] = [ { key: 'key', @@ -31,18 +27,11 @@ const registeredPanels: Panel[] = [ ]; describe('ExpandableFlyout', () => { - it(`shouldn't render flyout if no panels`, () => { - const state: State = { - panels: { - byId: {}, - }, - ui: { - pushVsOverlay: 'overlay', - }, - }; + it(`should not render flyout if window width is 0`, () => { + (useWindowWidth as jest.Mock).mockReturnValue(0); const result = render( - + ); @@ -50,122 +39,13 @@ describe('ExpandableFlyout', () => { expect(result.asFragment()).toMatchInlineSnapshot(``); }); - it('should render right section', () => { - const state = { - panels: { - byId: { - [id]: { - right: { - id: 'key', - }, - left: undefined, - preview: undefined, - }, - }, - }, - ui: { - pushVsOverlay: 'overlay' as const, - }, - }; - - const { getByTestId } = render( - - - - ); - - expect(getByTestId(RIGHT_SECTION_TEST_ID)).toBeInTheDocument(); - }); - - it('should render left section', () => { - const state = { - panels: { - byId: { - [id]: { - right: undefined, - left: { - id: 'key', - }, - preview: undefined, - }, - }, - }, - ui: { - pushVsOverlay: 'overlay' as const, - }, - }; - - const { getByTestId } = render( - - - - ); - - expect(getByTestId(LEFT_SECTION_TEST_ID)).toBeInTheDocument(); - }); - - it('should render preview section', () => { - const state = { - panels: { - byId: { - [id]: { - right: undefined, - left: undefined, - preview: [ - { - id: 'key', - }, - ], - }, - }, - }, - ui: { - pushVsOverlay: 'overlay' as const, - }, - }; - - const { getByTestId } = render( - - - - ); - - expect(getByTestId(PREVIEW_SECTION_TEST_ID)).toBeInTheDocument(); - }); + it(`should render flyout`, () => { + (useWindowWidth as jest.Mock).mockReturnValue(1000); - it('should not render flyout when right has value but does not matches registered panels', () => { const state = { panels: { byId: { - [id]: { - right: { - id: 'key1', - }, - left: undefined, - preview: undefined, - }, - }, - }, - ui: { - pushVsOverlay: 'overlay' as const, - }, - }; - - const { queryByTestId } = render( - - - - ); - - expect(queryByTestId('my-test-flyout')).toBeNull(); - expect(queryByTestId(RIGHT_SECTION_TEST_ID)).toBeNull(); - }); - - it('should render the menu to change display options', () => { - const state = { - panels: { - byId: { - [id]: { + [REDUX_ID_FOR_MEMORY_STORAGE]: { right: { id: 'key', }, @@ -174,17 +54,15 @@ describe('ExpandableFlyout', () => { }, }, }, - ui: { - pushVsOverlay: 'overlay' as const, - }, + ui: initialUiState, }; const { getByTestId } = render( - + ); - expect(getByTestId(SETTINGS_MENU_BUTTON_TEST_ID)).toBeInTheDocument(); + expect(getByTestId('TEST')).toBeInTheDocument(); }); }); diff --git a/packages/kbn-expandable-flyout/src/index.tsx b/packages/kbn-expandable-flyout/src/index.tsx index 4904661b2da88..25425a75e2ba9 100644 --- a/packages/kbn-expandable-flyout/src/index.tsx +++ b/packages/kbn-expandable-flyout/src/index.tsx @@ -10,23 +10,14 @@ import React, { useMemo } from 'react'; import type { Interpolation, Theme } from '@emotion/react'; import { EuiFlyoutProps } from '@elastic/eui'; -import { EuiFlexGroup, EuiFlyout } from '@elastic/eui'; +import { EuiFlyoutResizableProps } from '@elastic/eui/src/components/flyout/flyout_resizable'; +import { Container } from './components/container'; +import { useWindowWidth } from './hooks/use_window_width'; import { useInitializeFromLocalStorage } from './hooks/use_initialize_from_local_storage'; -import { FlyoutCustomProps, SettingsMenu } from './components/settings_menu'; -import { useSectionSizes } from './hooks/use_sections_sizes'; -import { useWindowSize } from './hooks/use_window_size'; -import { useExpandableFlyoutState } from './hooks/use_expandable_flyout_state'; -import { useExpandableFlyoutApi } from './hooks/use_expandable_flyout_api'; -import { PreviewSection } from './components/preview_section'; -import { RightSection } from './components/right_section'; -import type { FlyoutPanelProps, Panel } from './types'; -import { LeftSection } from './components/left_section'; -import { isPreviewBanner } from './components/preview_section'; -import { selectPushVsOverlay, useSelector } from './store/redux'; +import { FlyoutCustomProps } from './components/settings_menu'; +import type { Panel } from './types'; -const flyoutInnerStyles = { height: '100%' }; - -export interface ExpandableFlyoutProps extends Omit { +export interface ExpandableFlyoutProps extends Omit { /** * List of all registered panels available for render */ @@ -43,6 +34,10 @@ export interface ExpandableFlyoutProps extends Omit { * Set of properties that drive a settings menu */ flyoutCustomProps?: FlyoutCustomProps; + /** + * Optional data test subject string to be used on the EuiFlyoutResizable component + */ + 'data-test-subj'?: string; } /** @@ -52,108 +47,18 @@ export interface ExpandableFlyoutProps extends Omit { * The behavior expects that the left and preview sections should only be displayed is a right section * is already rendered. */ -export const ExpandableFlyout: React.FC = ({ - customStyles, - registeredPanels, - flyoutCustomProps, - ...flyoutProps -}) => { - const windowWidth = useWindowSize(); +export const ExpandableFlyout: React.FC = ({ ...props }) => { + const windowWidth = useWindowWidth(); useInitializeFromLocalStorage(); - // for flyout where the push vs overlay option is disable in the UI we fall back to overlay mode - const type = useSelector(selectPushVsOverlay); - const flyoutType = flyoutCustomProps?.pushVsOverlay?.disabled ? 'overlay' : type; - - const { left, right, preview } = useExpandableFlyoutState(); - const { closeFlyout } = useExpandableFlyoutApi(); - - const leftSection = useMemo( - () => registeredPanels.find((panel) => panel.key === left?.id), - [left, registeredPanels] - ); - - const rightSection = useMemo( - () => registeredPanels.find((panel) => panel.key === right?.id), - [right, registeredPanels] - ); - - // retrieve the last preview panel (most recent) - const mostRecentPreview = preview ? preview[preview.length - 1] : undefined; - const previewBanner = isPreviewBanner(mostRecentPreview?.params?.banner) - ? mostRecentPreview?.params?.banner - : undefined; + const container = useMemo(() => , [props]); - const previewSection = useMemo( - () => registeredPanels.find((panel) => panel.key === mostRecentPreview?.id), - [mostRecentPreview, registeredPanels] - ); - - const showRight = rightSection != null && right != null; - const showLeft = leftSection != null && left != null; - const showPreview = previewSection != null && preview != null; - - const { rightSectionWidth, leftSectionWidth, flyoutWidth, previewSectionLeft } = useSectionSizes({ - windowWidth, - showRight, - showLeft, - showPreview, - }); - - const hideFlyout = !(left && leftSection) && !(right && rightSection) && !preview?.length; - - if (hideFlyout) { + if (windowWidth === 0) { return null; } - return ( - { - closeFlyout(); - if (flyoutProps.onClose) { - flyoutProps.onClose(e); - } - }} - css={customStyles} - > - - {showLeft ? ( - - ) : null} - {showRight ? ( - - ) : null} - - - {showPreview ? ( - - ) : null} - - {!flyoutCustomProps?.hideSettings && } - - ); + return <>{container}; }; ExpandableFlyout.displayName = 'ExpandableFlyout'; diff --git a/packages/kbn-expandable-flyout/src/provider.test.tsx b/packages/kbn-expandable-flyout/src/provider.test.tsx index bdd4183c53276..7d7e6f8ab10c0 100644 --- a/packages/kbn-expandable-flyout/src/provider.test.tsx +++ b/packages/kbn-expandable-flyout/src/provider.test.tsx @@ -12,7 +12,7 @@ import { render } from '@testing-library/react'; import { TestProvider } from './test/provider'; import { UrlSynchronizer } from './provider'; import * as actions from './store/actions'; -import { State } from './store/state'; +import { initialUiState, State } from './store/state'; import { of } from 'rxjs'; const mockGet = jest.fn(); @@ -38,9 +38,7 @@ describe('UrlSynchronizer', () => { }, needsSync: true, }, - ui: { - pushVsOverlay: 'overlay', - }, + ui: initialUiState, }; render( @@ -64,9 +62,7 @@ describe('UrlSynchronizer', () => { byId: {}, needsSync: true, }, - ui: { - pushVsOverlay: 'overlay', - }, + ui: initialUiState, }; render( @@ -101,9 +97,7 @@ describe('UrlSynchronizer', () => { }, needsSync: true, }, - ui: { - pushVsOverlay: 'overlay', - }, + ui: initialUiState, }; render( diff --git a/packages/kbn-expandable-flyout/src/store/actions.ts b/packages/kbn-expandable-flyout/src/store/actions.ts index 2886118369b0e..49e28befa3456 100644 --- a/packages/kbn-expandable-flyout/src/store/actions.ts +++ b/packages/kbn-expandable-flyout/src/store/actions.ts @@ -23,6 +23,15 @@ export enum ActionType { urlChanged = 'urlChanged', changePushVsOverlay = 'change_push_overlay', + + setDefaultWidths = 'set_default_widths', + + changeUserCollapsedWidth = 'change_user_collapsed_width', + changeUserExpandedWidth = 'change_user_expanded_width', + + changeUserSectionWidths = 'change_user_section_widths', + + resetAllUserWidths = 'reset_all_user_widths', } export const openPanelsAction = createAction<{ @@ -134,3 +143,57 @@ export const changePushVsOverlayAction = createAction<{ */ savedToLocalStorage: boolean; }>(ActionType.changePushVsOverlay); + +export const setDefaultWidthsAction = createAction<{ + /** + * Default width for the right section + */ + right: number; + /** + * Default width for the left section + */ + left: number; + /** + * Default width for the preview section + */ + preview: number; +}>(ActionType.setDefaultWidths); + +export const changeUserCollapsedWidthAction = createAction<{ + /** + * Width of the collapsed flyout + */ + width: number; + /** + * Used in the redux middleware to decide if the value needs to be saved to local storage. + */ + savedToLocalStorage: boolean; +}>(ActionType.changeUserCollapsedWidth); + +export const changeUserExpandedWidthAction = createAction<{ + /** + * Width of the expanded flyout + */ + width: number; + /** + * Used in the redux middleware to decide if the value needs to be saved to local storage. + */ + savedToLocalStorage: boolean; +}>(ActionType.changeUserExpandedWidth); + +export const changeUserSectionWidthsAction = createAction<{ + /** + * Width of the left section + */ + left: number; + /** + * Width of the right section + */ + right: number; + /** + * Used in the redux middleware to decide if the value needs to be saved to local storage. + */ + savedToLocalStorage: boolean; +}>(ActionType.changeUserSectionWidths); + +export const resetAllUserChangedWidthsAction = createAction(ActionType.resetAllUserWidths); diff --git a/packages/kbn-expandable-flyout/src/store/middlewares.test.ts b/packages/kbn-expandable-flyout/src/store/middlewares.test.ts index ccbb5d5443db7..680a5619b0b64 100644 --- a/packages/kbn-expandable-flyout/src/store/middlewares.test.ts +++ b/packages/kbn-expandable-flyout/src/store/middlewares.test.ts @@ -8,10 +8,27 @@ */ import { localStorageMock } from '../../__mocks__'; -import { EXPANDABLE_FLYOUT_LOCAL_STORAGE, PUSH_VS_OVERLAY_LOCAL_STORAGE } from '../constants'; -import { savePushVsOverlayToLocalStorageMiddleware } from './middlewares'; -import { createAction, type MiddlewareAPI } from '@reduxjs/toolkit'; -import { changePushVsOverlayAction } from './actions'; +import { + EXPANDABLE_FLYOUT_LOCAL_STORAGE, + PUSH_VS_OVERLAY_LOCAL_STORAGE, + USER_COLLAPSED_WIDTH_LOCAL_STORAGE, + USER_EXPANDED_WIDTH_LOCAL_STORAGE, + USER_SECTION_WIDTHS_LOCAL_STORAGE, +} from '../constants'; +import { + clearAllUserWidthsFromLocalStorageMiddleware, + savePushVsOverlayToLocalStorageMiddleware, + saveUserFlyoutWidthsToLocalStorageMiddleware, + saveUserSectionWidthsToLocalStorageMiddleware, +} from './middlewares'; +import { createAction } from '@reduxjs/toolkit'; +import { + changeUserCollapsedWidthAction, + changeUserExpandedWidthAction, + changeUserSectionWidthsAction, + changePushVsOverlayAction, + resetAllUserChangedWidthsAction, +} from './actions'; const noTypeAction = createAction<{ type: 'no_type'; @@ -20,40 +37,182 @@ const randomAction = createAction<{ type: 'random_type'; }>('random_action'); -describe('pushVsOverlayMiddleware', () => { +describe('middlewares', () => { beforeEach(() => { Object.defineProperty(window, 'localStorage', { value: localStorageMock(), }); }); - it('should ignore action without type', () => { - savePushVsOverlayToLocalStorageMiddleware({} as MiddlewareAPI)(jest.fn)(noTypeAction); + describe('savePushVsOverlayToLocalStorageMiddleware', () => { + it('should ignore action without type', () => { + savePushVsOverlayToLocalStorageMiddleware()(jest.fn)(noTypeAction); - expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); + expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); + }); + + it('should ignore action of types other than changePushVsOverlayAction', () => { + savePushVsOverlayToLocalStorageMiddleware()(jest.fn)(randomAction); + + expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); + }); + + it('should save value to local storage if action is of type changePushVsOverlayAction', () => { + savePushVsOverlayToLocalStorageMiddleware()(jest.fn)( + changePushVsOverlayAction({ type: 'push', savedToLocalStorage: true }) + ); + + expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual( + JSON.stringify({ [PUSH_VS_OVERLAY_LOCAL_STORAGE]: 'push' }) + ); + }); + + it('should not save value to local storage if savedToLocalStorage is false', () => { + savePushVsOverlayToLocalStorageMiddleware()(jest.fn)( + changePushVsOverlayAction({ type: 'push', savedToLocalStorage: false }) + ); + expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); + }); }); - it('should ignore action of types other than changePushVsOverlayAction', () => { - savePushVsOverlayToLocalStorageMiddleware({} as MiddlewareAPI)(jest.fn)(randomAction); + describe('saveUserFlyoutWidthsToLocalStorageMiddleware', () => { + it('should ignore action without type', () => { + saveUserFlyoutWidthsToLocalStorageMiddleware()(jest.fn)(noTypeAction); + + expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); + }); + + it('should ignore action of other types', () => { + saveUserFlyoutWidthsToLocalStorageMiddleware()(jest.fn)(randomAction); - expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); + expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); + }); + + it('should save collapsed value to local storage if action is of type changeUserCollapsedWidthAction', () => { + saveUserFlyoutWidthsToLocalStorageMiddleware()(jest.fn)( + changeUserCollapsedWidthAction({ width: 250, savedToLocalStorage: true }) + ); + + const expandableFlyout = localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE); + expect(expandableFlyout).not.toBe(null); + + if (expandableFlyout) { + expect(JSON.parse(expandableFlyout)[USER_COLLAPSED_WIDTH_LOCAL_STORAGE]).toEqual(250); + } + }); + + it('should save expanded value to local storage if action is of type changeUserExpandedWidthAction', () => { + saveUserFlyoutWidthsToLocalStorageMiddleware()(jest.fn)( + changeUserExpandedWidthAction({ width: 500, savedToLocalStorage: true }) + ); + + const expandableFlyout = localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE); + expect(expandableFlyout).not.toBe(null); + + if (expandableFlyout) { + expect(JSON.parse(expandableFlyout)[USER_EXPANDED_WIDTH_LOCAL_STORAGE]).toEqual(500); + } + }); + + it('should not save collapsed value to local storage if savedToLocalStorage is false', () => { + saveUserFlyoutWidthsToLocalStorageMiddleware()(jest.fn)( + changeUserCollapsedWidthAction({ width: 250, savedToLocalStorage: false }) + ); + + expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); + }); + + it('should not save expanded value to local storage if savedToLocalStorage is false', () => { + saveUserFlyoutWidthsToLocalStorageMiddleware()(jest.fn)( + changeUserExpandedWidthAction({ width: 500, savedToLocalStorage: false }) + ); + + expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); + }); }); - it('should save value to local storage if action is of type changePushVsOverlayAction', () => { - savePushVsOverlayToLocalStorageMiddleware({} as MiddlewareAPI)(jest.fn)( - changePushVsOverlayAction({ type: 'push', savedToLocalStorage: true }) - ); + describe('saveUserSectionWidthsToLocalStorageMiddleware', () => { + it('should ignore action without type', () => { + saveUserSectionWidthsToLocalStorageMiddleware()(jest.fn)(noTypeAction); + + expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); + }); + + it('should ignore action of other types ', () => { + saveUserSectionWidthsToLocalStorageMiddleware()(jest.fn)(randomAction); + + expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); + }); + + it('should save section width values to local storage if action is of type changeUserSectionWidthsAction', () => { + saveUserSectionWidthsToLocalStorageMiddleware()(jest.fn)( + changeUserSectionWidthsAction({ + left: 500, + right: 500, + savedToLocalStorage: true, + }) + ); + + const expandableFlyout = localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE); + expect(expandableFlyout).not.toBe(null); + + if (expandableFlyout) { + expect(JSON.parse(expandableFlyout)[USER_SECTION_WIDTHS_LOCAL_STORAGE]).toEqual({ + left: 500, + right: 500, + }); + } + }); - expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual( - JSON.stringify({ [PUSH_VS_OVERLAY_LOCAL_STORAGE]: 'push' }) - ); + it('should not save section width values to local storage if savedToLocalStorage is false', () => { + saveUserSectionWidthsToLocalStorageMiddleware()(jest.fn)( + changeUserSectionWidthsAction({ + left: 500, + right: 500, + savedToLocalStorage: false, + }) + ); + + expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); + }); }); - it('should not save value to local storage if savedToLocalStorage is false', () => { - savePushVsOverlayToLocalStorageMiddleware({} as MiddlewareAPI)(jest.fn)( - changePushVsOverlayAction({ type: 'push', savedToLocalStorage: false }) - ); + describe('clearAllUserWidthsFromLocalStorageMiddleware', () => { + it('should ignore action without type', () => { + clearAllUserWidthsFromLocalStorageMiddleware()(jest.fn)(noTypeAction); + + expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); + }); + + it('should ignore action of other types ', () => { + clearAllUserWidthsFromLocalStorageMiddleware()(jest.fn)(randomAction); - expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); + expect(localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE)).toEqual(null); + }); + + it('should clear width values from local storage if action is of type resetUserCollapsedWidthAction', () => { + localStorage.setItem( + EXPANDABLE_FLYOUT_LOCAL_STORAGE, + JSON.stringify({ + [PUSH_VS_OVERLAY_LOCAL_STORAGE]: 'push', + [USER_COLLAPSED_WIDTH_LOCAL_STORAGE]: 250, + [USER_EXPANDED_WIDTH_LOCAL_STORAGE]: 500, + [USER_SECTION_WIDTHS_LOCAL_STORAGE]: { left: 50, right: 50 }, + }) + ); + + clearAllUserWidthsFromLocalStorageMiddleware()(jest.fn)(resetAllUserChangedWidthsAction()); + + const expandableFlyout = localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE); + expect(expandableFlyout).not.toBe(null); + + if (expandableFlyout) { + const parsed = JSON.parse(expandableFlyout); + expect(parsed[PUSH_VS_OVERLAY_LOCAL_STORAGE]).toEqual('push'); + expect(expandableFlyout).not.toHaveProperty(USER_COLLAPSED_WIDTH_LOCAL_STORAGE); + expect(expandableFlyout).not.toHaveProperty(USER_EXPANDED_WIDTH_LOCAL_STORAGE); + expect(expandableFlyout).not.toHaveProperty(USER_SECTION_WIDTHS_LOCAL_STORAGE); + } + }); }); }); diff --git a/packages/kbn-expandable-flyout/src/store/middlewares.ts b/packages/kbn-expandable-flyout/src/store/middlewares.ts index c9e04ea2846d7..4fb5354535caf 100644 --- a/packages/kbn-expandable-flyout/src/store/middlewares.ts +++ b/packages/kbn-expandable-flyout/src/store/middlewares.ts @@ -7,26 +7,112 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { Action, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit'; -import { changePushVsOverlayAction } from './actions'; -import { EXPANDABLE_FLYOUT_LOCAL_STORAGE, PUSH_VS_OVERLAY_LOCAL_STORAGE } from '../constants'; +import type { Action, Dispatch } from '@reduxjs/toolkit'; +import { + changeUserCollapsedWidthAction, + changeUserExpandedWidthAction, + changeUserSectionWidthsAction, + changePushVsOverlayAction, + resetAllUserChangedWidthsAction, +} from './actions'; +import { + USER_COLLAPSED_WIDTH_LOCAL_STORAGE, + EXPANDABLE_FLYOUT_LOCAL_STORAGE, + USER_SECTION_WIDTHS_LOCAL_STORAGE, + PUSH_VS_OVERLAY_LOCAL_STORAGE, + USER_EXPANDED_WIDTH_LOCAL_STORAGE, +} from '../constants'; /** * Middleware to save the push vs overlay state to local storage */ export const savePushVsOverlayToLocalStorageMiddleware = - (store: MiddlewareAPI) => (next: Dispatch) => (action: Action) => { + () => (next: Dispatch) => (action: Action) => { if (!action.type) { return next(action); } if (changePushVsOverlayAction.match(action) && action.payload.savedToLocalStorage) { - localStorage.setItem( - EXPANDABLE_FLYOUT_LOCAL_STORAGE, - JSON.stringify({ - [PUSH_VS_OVERLAY_LOCAL_STORAGE]: action.payload.type, - }) - ); + const currentStringValue = localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE); + const currentJsonValue = currentStringValue ? JSON.parse(currentStringValue) : {}; + + currentJsonValue[PUSH_VS_OVERLAY_LOCAL_STORAGE] = action.payload.type; + + localStorage.setItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE, JSON.stringify(currentJsonValue)); + } + + return next(action); + }; + +/** + * Middleware to save the user collapsed and expanded flyout widths to local storage + */ +export const saveUserFlyoutWidthsToLocalStorageMiddleware = + () => (next: Dispatch) => (action: Action) => { + if (!action.type) { + return next(action); + } + + const currentStringValue = localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE); + const currentJsonValue = currentStringValue ? JSON.parse(currentStringValue) : {}; + + if (changeUserCollapsedWidthAction.match(action) && action.payload.savedToLocalStorage) { + currentJsonValue[USER_COLLAPSED_WIDTH_LOCAL_STORAGE] = action.payload.width; + + localStorage.setItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE, JSON.stringify(currentJsonValue)); + } + + if (changeUserExpandedWidthAction.match(action) && action.payload.savedToLocalStorage) { + currentJsonValue[USER_EXPANDED_WIDTH_LOCAL_STORAGE] = action.payload.width; + + localStorage.setItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE, JSON.stringify(currentJsonValue)); + } + + return next(action); + }; + +/** + * Middleware to save the user left and right section widths to local storage + */ +export const saveUserSectionWidthsToLocalStorageMiddleware = + () => (next: Dispatch) => (action: Action) => { + if (!action.type) { + return next(action); + } + + if (changeUserSectionWidthsAction.match(action) && action.payload.savedToLocalStorage) { + const currentStringValue = localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE); + const currentJsonValue = currentStringValue ? JSON.parse(currentStringValue) : {}; + + currentJsonValue[USER_SECTION_WIDTHS_LOCAL_STORAGE] = { + left: action.payload.left, + right: action.payload.right, + }; + + localStorage.setItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE, JSON.stringify(currentJsonValue)); + } + + return next(action); + }; + +/** + * Middleware to save the user left and right section widths to local storage + */ +export const clearAllUserWidthsFromLocalStorageMiddleware = + () => (next: Dispatch) => (action: Action) => { + if (!action.type) { + return next(action); + } + + if (resetAllUserChangedWidthsAction.match(action)) { + const currentStringValue = localStorage.getItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE); + const currentJsonValue = currentStringValue ? JSON.parse(currentStringValue) : {}; + + delete currentJsonValue[USER_COLLAPSED_WIDTH_LOCAL_STORAGE]; + delete currentJsonValue[USER_EXPANDED_WIDTH_LOCAL_STORAGE]; + delete currentJsonValue[USER_SECTION_WIDTHS_LOCAL_STORAGE]; + + localStorage.setItem(EXPANDABLE_FLYOUT_LOCAL_STORAGE, JSON.stringify(currentJsonValue)); } return next(action); diff --git a/packages/kbn-expandable-flyout/src/store/reducers.test.ts b/packages/kbn-expandable-flyout/src/store/reducers.test.ts index 78caea13bd2d3..1a887333daca8 100644 --- a/packages/kbn-expandable-flyout/src/store/reducers.test.ts +++ b/packages/kbn-expandable-flyout/src/store/reducers.test.ts @@ -12,6 +12,9 @@ import { panelsReducer, uiReducer } from './reducers'; import { initialPanelsState, PanelsState, initialUiState, UiState } from './state'; import { changePushVsOverlayAction, + changeUserCollapsedWidthAction, + changeUserExpandedWidthAction, + changeUserSectionWidthsAction, closeLeftPanelAction, closePanelsAction, closePreviewPanelAction, @@ -21,6 +24,8 @@ import { openPreviewPanelAction, openRightPanelAction, previousPreviewPanelAction, + resetAllUserChangedWidthsAction, + setDefaultWidthsAction, } from './actions'; const id1 = 'id1'; @@ -794,12 +799,14 @@ describe('uiReducer', () => { const newState: UiState = uiReducer(state, action); expect(newState).toEqual({ + ...state, pushVsOverlay: 'push', }); }); it('should override value if id already exists', () => { const state: UiState = { + ...initialUiState, pushVsOverlay: 'push', }; const action = changePushVsOverlayAction({ @@ -809,8 +816,239 @@ describe('uiReducer', () => { const newState: UiState = uiReducer(state, action); expect(newState).toEqual({ + ...state, pushVsOverlay: 'overlay', }); }); }); + + describe('should handle setDefaultWidthsAction action', () => { + it('should set value state is empty', () => { + const state: UiState = initialUiState; + const action = setDefaultWidthsAction({ + right: 200, + left: 600, + preview: 200, + }); + const newState: UiState = uiReducer(state, action); + + expect(newState).toEqual({ + ...state, + defaultWidths: { + rightWidth: 200, + leftWidth: 600, + previewWidth: 200, + rightPercentage: 25, + leftPercentage: 75, + previewPercentage: 25, + }, + }); + }); + + it('should override value if state not empty', () => { + const state: UiState = { + ...initialUiState, + defaultWidths: { + rightWidth: 200, + leftWidth: 600, + previewWidth: 200, + rightPercentage: 25, + leftPercentage: 75, + previewPercentage: 25, + }, + }; + const action = setDefaultWidthsAction({ + right: 500, + left: 500, + preview: 500, + }); + const newState: UiState = uiReducer(state, action); + + expect(newState).toEqual({ + ...state, + defaultWidths: { + rightWidth: 500, + leftWidth: 500, + previewWidth: 500, + rightPercentage: 50, + leftPercentage: 50, + previewPercentage: 50, + }, + }); + }); + }); + + describe('should handle changeUserCollapsedWidthAction action', () => { + it('should set value state is empty', () => { + const state: UiState = initialUiState; + const action = changeUserCollapsedWidthAction({ + width: 200, + savedToLocalStorage: false, + }); + const newState: UiState = uiReducer(state, action); + + expect(newState).toEqual({ + ...state, + userFlyoutWidths: { + collapsedWidth: 200, + }, + }); + }); + + it('should override value if state not empty', () => { + const state: UiState = { + ...initialUiState, + userFlyoutWidths: { + collapsedWidth: 200, + expandedWidth: 500, + }, + }; + const action = changeUserCollapsedWidthAction({ + width: 250, + savedToLocalStorage: false, + }); + const newState: UiState = uiReducer(state, action); + + expect(newState).toEqual({ + ...state, + userFlyoutWidths: { + collapsedWidth: 250, + expandedWidth: 500, + }, + }); + }); + }); + + describe('should handle changeUserExpandedWidthAction action', () => { + it('should set value state is empty', () => { + const state: UiState = initialUiState; + const action = changeUserExpandedWidthAction({ + width: 500, + savedToLocalStorage: false, + }); + const newState: UiState = uiReducer(state, action); + + expect(newState).toEqual({ + ...state, + userFlyoutWidths: { + expandedWidth: 500, + }, + }); + }); + + it('should override value if state not empty', () => { + const state: UiState = { + ...initialUiState, + userFlyoutWidths: { + collapsedWidth: 200, + expandedWidth: 500, + }, + }; + const action = changeUserExpandedWidthAction({ + width: 1000, + savedToLocalStorage: false, + }); + const newState: UiState = uiReducer(state, action); + + expect(newState).toEqual({ + ...state, + userFlyoutWidths: { + collapsedWidth: 200, + expandedWidth: 1000, + }, + }); + }); + }); + + describe('should handle changeUserSectionWidthsAction action', () => { + it('should set value state is empty', () => { + const state: UiState = initialUiState; + const action = changeUserSectionWidthsAction({ + right: 50, + left: 50, + savedToLocalStorage: false, + }); + const newState: UiState = uiReducer(state, action); + + expect(newState).toEqual({ + ...state, + userSectionWidths: { + leftPercentage: 50, + rightPercentage: 50, + }, + }); + }); + + it('should override value if state not empty', () => { + const state: UiState = { + ...initialUiState, + userSectionWidths: { + leftPercentage: 50, + rightPercentage: 50, + }, + }; + const action = changeUserSectionWidthsAction({ + right: 30, + left: 70, + savedToLocalStorage: false, + }); + const newState: UiState = uiReducer(state, action); + + expect(newState).toEqual({ + ...state, + userSectionWidths: { + leftPercentage: 70, + rightPercentage: 30, + }, + }); + }); + }); + + describe('should handle resetAllUserChangedWidthsAction action', () => { + it('should set value state is empty', () => { + const state: UiState = initialUiState; + const action = resetAllUserChangedWidthsAction(); + const newState: UiState = uiReducer(state, action); + + expect(newState).toEqual({ + ...state, + userSectionWidths: { + leftPercentage: undefined, + rightPercentage: undefined, + }, + userFlyoutWidths: { + collapsedWidth: undefined, + expandedWidth: undefined, + }, + }); + }); + + it('should override value if state not empty', () => { + const state: UiState = { + ...initialUiState, + userFlyoutWidths: { + collapsedWidth: 200, + expandedWidth: 500, + }, + userSectionWidths: { + leftPercentage: 50, + rightPercentage: 50, + }, + }; + const action = resetAllUserChangedWidthsAction(); + const newState: UiState = uiReducer(state, action); + + expect(newState).toEqual({ + ...state, + userSectionWidths: { + leftPercentage: undefined, + rightPercentage: undefined, + }, + userFlyoutWidths: { + collapsedWidth: undefined, + expandedWidth: undefined, + }, + }); + }); + }); }); diff --git a/packages/kbn-expandable-flyout/src/store/reducers.ts b/packages/kbn-expandable-flyout/src/store/reducers.ts index 54918f5c6d7bb..b14aa0b1b703b 100644 --- a/packages/kbn-expandable-flyout/src/store/reducers.ts +++ b/packages/kbn-expandable-flyout/src/store/reducers.ts @@ -21,6 +21,11 @@ import { openPreviewPanelAction, urlChangedAction, changePushVsOverlayAction, + setDefaultWidthsAction, + changeUserCollapsedWidthAction, + changeUserExpandedWidthAction, + changeUserSectionWidthsAction, + resetAllUserChangedWidthsAction, } from './actions'; import { initialPanelsState, initialUiState } from './state'; @@ -155,4 +160,33 @@ export const uiReducer = createReducer(initialUiState, (builder) => { builder.addCase(changePushVsOverlayAction, (state, { payload: { type } }) => { state.pushVsOverlay = type; }); + + builder.addCase(setDefaultWidthsAction, (state, { payload: { right, left, preview } }) => { + state.defaultWidths.rightWidth = right; + state.defaultWidths.leftWidth = left; + state.defaultWidths.previewWidth = preview; + state.defaultWidths.rightPercentage = (right / (right + left)) * 100; + state.defaultWidths.leftPercentage = (left / (right + left)) * 100; + state.defaultWidths.previewPercentage = (right / (right + left)) * 100; + }); + + builder.addCase(changeUserCollapsedWidthAction, (state, { payload: { width } }) => { + state.userFlyoutWidths.collapsedWidth = width; + }); + + builder.addCase(changeUserExpandedWidthAction, (state, { payload: { width } }) => { + state.userFlyoutWidths.expandedWidth = width; + }); + + builder.addCase(changeUserSectionWidthsAction, (state, { payload: { right, left } }) => { + state.userSectionWidths.leftPercentage = left; + state.userSectionWidths.rightPercentage = right; + }); + + builder.addCase(resetAllUserChangedWidthsAction, (state) => { + state.userFlyoutWidths.collapsedWidth = undefined; + state.userFlyoutWidths.expandedWidth = undefined; + state.userSectionWidths.leftPercentage = undefined; + state.userSectionWidths.rightPercentage = undefined; + }); }); diff --git a/packages/kbn-expandable-flyout/src/store/redux.ts b/packages/kbn-expandable-flyout/src/store/redux.ts index 9951334a247f3..d68b4a0295769 100644 --- a/packages/kbn-expandable-flyout/src/store/redux.ts +++ b/packages/kbn-expandable-flyout/src/store/redux.ts @@ -13,7 +13,12 @@ import { configureStore } from '@reduxjs/toolkit'; import { createSelector } from 'reselect'; import { panelsReducer, uiReducer } from './reducers'; import { initialState, State } from './state'; -import { savePushVsOverlayToLocalStorageMiddleware } from './middlewares'; +import { + savePushVsOverlayToLocalStorageMiddleware, + saveUserSectionWidthsToLocalStorageMiddleware, + saveUserFlyoutWidthsToLocalStorageMiddleware, + clearAllUserWidthsFromLocalStorageMiddleware, +} from './middlewares'; export const store = configureStore({ reducer: { @@ -21,7 +26,12 @@ export const store = configureStore({ ui: uiReducer, }, devTools: process.env.NODE_ENV !== 'production', - middleware: [savePushVsOverlayToLocalStorageMiddleware], + middleware: [ + savePushVsOverlayToLocalStorageMiddleware, + saveUserSectionWidthsToLocalStorageMiddleware, + saveUserFlyoutWidthsToLocalStorageMiddleware, + clearAllUserWidthsFromLocalStorageMiddleware, + ], }); export const Context = createContext>({ @@ -41,3 +51,9 @@ export const selectNeedsSync = () => createSelector(panelsSelector, (state) => s const uiSelector = createSelector(stateSelector, (state) => state.ui); export const selectPushVsOverlay = createSelector(uiSelector, (state) => state.pushVsOverlay); +export const selectDefaultWidths = createSelector(uiSelector, (state) => state.defaultWidths); +export const selectUserFlyoutWidths = createSelector(uiSelector, (state) => state.userFlyoutWidths); +export const selectUserSectionWidths = createSelector( + uiSelector, + (state) => state.userSectionWidths +); diff --git a/packages/kbn-expandable-flyout/src/store/state.ts b/packages/kbn-expandable-flyout/src/store/state.ts index a794d0db34d28..e158f61aaccd5 100644 --- a/packages/kbn-expandable-flyout/src/store/state.ts +++ b/packages/kbn-expandable-flyout/src/store/state.ts @@ -44,15 +44,79 @@ export const initialPanelsState: PanelsState = { needsSync: false, }; +export interface DefaultWidthsState { + /** + * Default width for the right section (calculated from the window width) + */ + rightWidth: number; + /** + * Default width for the left section (calculated from the window width) + */ + leftWidth: number; + /** + * Default width for the preview section (calculated from the window width) + */ + previewWidth: number; + /** + * Value of the right width in percentage (of the flyout total width) + */ + rightPercentage: number; + /** + * Value of the left width in percentage (of the flyout total width) + */ + leftPercentage: number; + /** + * Value of the preview width in percentage (of the flyout total width) + */ + previewPercentage: number; +} + +export interface UserFlyoutWidthsState { + /** + * Width of the collapsed flyout + */ + collapsedWidth?: number; + /** + * Width of the expanded flyout + */ + expandedWidth?: number; +} + +export interface UserSectionWidthsState { + /** + * Percentage for the left section + */ + leftPercentage: number | undefined; + /** + * Percentage for the right section + */ + rightPercentage: number | undefined; +} + export interface UiState { /** * Push vs overlay information */ pushVsOverlay: 'push' | 'overlay'; + /** + * Default widths for the flyout + */ + defaultWidths: DefaultWidthsState; + /** + * User resized widths for the flyout + */ + userFlyoutWidths: UserFlyoutWidthsState; + /** + * User resized left and right section widths for the flyout + */ + userSectionWidths: UserSectionWidthsState; } export const initialUiState: UiState = { pushVsOverlay: 'overlay', + defaultWidths: {} as DefaultWidthsState, + userFlyoutWidths: {}, + userSectionWidths: {} as UserSectionWidthsState, }; export interface State { diff --git a/packages/kbn-expandable-flyout/src/test/provider.tsx b/packages/kbn-expandable-flyout/src/test/provider.tsx index 0dc2656e15c7e..81de83720afd7 100644 --- a/packages/kbn-expandable-flyout/src/test/provider.tsx +++ b/packages/kbn-expandable-flyout/src/test/provider.tsx @@ -11,7 +11,11 @@ import { Provider as ReduxProvider } from 'react-redux'; import { configureStore } from '@reduxjs/toolkit'; import React, { FC, PropsWithChildren } from 'react'; import { I18nProvider } from '@kbn/i18n-react'; -import { savePushVsOverlayToLocalStorageMiddleware } from '../store/middlewares'; +import { + savePushVsOverlayToLocalStorageMiddleware, + saveUserSectionWidthsToLocalStorageMiddleware, + saveUserFlyoutWidthsToLocalStorageMiddleware, +} from '../store/middlewares'; import { ExpandableFlyoutContextProvider } from '../context'; import { panelsReducer, uiReducer } from '../store/reducers'; import { Context } from '../store/redux'; @@ -34,7 +38,11 @@ export const TestProvider: FC> = ({ }, devTools: false, preloadedState: state, - middleware: [savePushVsOverlayToLocalStorageMiddleware], + middleware: [ + savePushVsOverlayToLocalStorageMiddleware, + saveUserSectionWidthsToLocalStorageMiddleware, + saveUserFlyoutWidthsToLocalStorageMiddleware, + ], }); return ( From 518533898ad53f8122c210f27a29f02790191733 Mon Sep 17 00:00:00 2001 From: Elena Shostak <165678770+elena-shostak@users.noreply.github.com> Date: Thu, 19 Sep 2024 17:42:43 +0200 Subject: [PATCH 12/19] [8.x] Added scope field to features config. (#191634) (#193389) # Backport This will backport the following commits from `main` to `8.x`: - [Added scope field to features config. (#191634)](https://github.com/elastic/kibana/pull/191634) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Elastic Machine --- .../security/feature-registration.asciidoc | 5 + .../feature_control_examples/server/plugin.ts | 2 + .../selection/server/plugin.ts | 2 + .../guided_onboarding/server/feature.ts | 2 + .../alerting_example/server/plugin.ts | 2 + .../features/src/assistant/kibana_features.ts | 2 + .../src/attack_discovery/kibana_features.ts | 2 + .../features/src/cases/kibana_features.ts | 2 + .../features/src/security/kibana_features.ts | 2 + x-pack/plugins/actions/server/feature.ts | 2 + .../server/maintenance_window_feature.ts | 2 + .../rules_settings_feature.test.ts | 2 + .../rules_settings/rules_settings_feature.ts | 2 + x-pack/plugins/canvas/server/feature.test.ts | 8 + x-pack/plugins/canvas/server/feature.ts | 2 + x-pack/plugins/cases/server/features.ts | 2 + .../enterprise_search/server/plugin.ts | 2 + x-pack/plugins/features/common/index.ts | 2 +- .../plugins/features/common/kibana_feature.ts | 19 ++ x-pack/plugins/features/public/index.ts | 2 +- .../features/server/feature_registry.test.ts | 19 ++ .../features/server/feature_registry.ts | 5 + .../plugins/features/server/feature_schema.ts | 33 +++- .../plugins/features/server/oss_features.ts | 11 ++ x-pack/plugins/features/server/plugin.test.ts | 3 + x-pack/plugins/fleet/server/plugin.ts | 3 + x-pack/plugins/graph/server/plugin.ts | 2 + x-pack/plugins/maps/server/plugin.ts | 2 + x-pack/plugins/ml/server/plugin.ts | 2 + x-pack/plugins/monitoring/server/plugin.ts | 2 + .../apm/server/feature.ts | 2 + .../infra/server/features.ts | 3 + .../observability/server/plugin.ts | 3 + .../server/plugin.ts | 2 + .../profiling/server/feature.ts | 2 + .../slo/server/plugin.ts | 2 + .../synthetics/server/feature.ts | 2 + .../osquery/server/utils/register_features.ts | 2 + x-pack/plugins/reporting/server/features.ts | 2 + .../plugins/reporting/server/plugin.test.ts | 1 + .../saved_objects_tagging/server/features.ts | 2 + .../server/plugin.ts | 2 + .../enabled_features.test.tsx.snap | 8 + .../enabled_features.test.tsx | 3 + .../capabilities_switcher.test.ts | 3 + x-pack/plugins/spaces/server/plugin.test.ts | 6 +- x-pack/plugins/spaces/server/plugin.ts | 4 +- .../routes/api/external/copy_to_space.test.ts | 3 +- .../server/routes/api/external/delete.test.ts | 3 +- .../disable_legacy_url_aliases.test.ts | 3 +- .../server/routes/api/external/get.test.ts | 3 +- .../routes/api/external/get_all.test.ts | 3 +- .../external/get_shareable_references.test.ts | 3 +- .../server/routes/api/external/post.test.ts | 7 +- .../server/routes/api/external/put.test.ts | 7 +- .../external/update_objects_spaces.test.ts | 3 +- .../api/internal/get_content_summary.test.ts | 3 +- .../api/internal/set_solution_space.test.ts | 3 +- .../spaces_client/spaces_client.test.ts | 176 +++++++++++++++--- .../server/spaces_client/spaces_client.ts | 31 ++- .../spaces_client_service.test.ts | 11 +- .../spaces_client/spaces_client_service.ts | 6 +- .../spaces_service/spaces_service.test.ts | 7 +- x-pack/plugins/stack_alerts/server/feature.ts | 2 + .../actions_simulators/server/plugin.ts | 2 + .../common/plugins/alerts/server/plugin.ts | 2 + .../alerts_restricted/server/plugin.ts | 2 + .../apis/features/features/features.ts | 58 ++++++ .../common/plugins/cases/server/plugin.ts | 2 + .../plugins/observability/server/plugin.ts | 2 + .../security_solution/server/plugin.ts | 3 + .../plugins/alerts/server/plugin.ts | 2 + .../plugins/alerts/server/plugin.ts | 2 + .../common/plugins/foo_plugin/server/index.ts | 2 + 74 files changed, 490 insertions(+), 58 deletions(-) diff --git a/docs/developer/architecture/security/feature-registration.asciidoc b/docs/developer/architecture/security/feature-registration.asciidoc index c9d173a6be6fb..b16142311cc75 100644 --- a/docs/developer/architecture/security/feature-registration.asciidoc +++ b/docs/developer/architecture/security/feature-registration.asciidoc @@ -59,6 +59,11 @@ of features within the management screens. |See <> |The set of subfeatures that enables finer access control than the `all` and `read` feature privileges. These options are only available in the Gold subscription level and higher. +|`scope` (optional) +|`string[]` +|`["spaces", "security"]` +| Default `security`. Scope identifies if feature should appear in both Spaces Visibility Toggles and Security Feature Privileges or only in Security Feature Privileges. + |=== ==== Privilege definition diff --git a/examples/feature_control_examples/server/plugin.ts b/examples/feature_control_examples/server/plugin.ts index 15804b8c2e5ac..f7cde857840f4 100644 --- a/examples/feature_control_examples/server/plugin.ts +++ b/examples/feature_control_examples/server/plugin.ts @@ -12,6 +12,7 @@ import { FeaturesPluginSetup, // PluginStartContract as FeaturesPluginStart, } from '@kbn/features-plugin/server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { FEATURE_PRIVILEGES_PLUGIN_ID } from '../common'; export interface FeatureControlExampleDeps { @@ -27,6 +28,7 @@ export class FeatureControlsPluginExample name: 'Feature Plugin Examples', category: DEFAULT_APP_CATEGORIES.management, app: ['FeaturePluginExample'], + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], privileges: { all: { app: ['FeaturePluginExample'], diff --git a/src/plugins/ai_assistant_management/selection/server/plugin.ts b/src/plugins/ai_assistant_management/selection/server/plugin.ts index 929e09047269c..a8175f2f0bce8 100644 --- a/src/plugins/ai_assistant_management/selection/server/plugin.ts +++ b/src/plugins/ai_assistant_management/selection/server/plugin.ts @@ -17,6 +17,7 @@ import { DEFAULT_APP_CATEGORIES, } from '@kbn/core/server'; import { schema } from '@kbn/config-schema'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import type { AIAssistantManagementSelectionConfig } from './config'; import type { AIAssistantManagementSelectionPluginServerDependenciesSetup, @@ -111,6 +112,7 @@ export class AIAssistantManagementSelectionPlugin order: 8600, app: [], category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], management: { kibana: [ 'aiAssistantManagementSelection', diff --git a/src/plugins/guided_onboarding/server/feature.ts b/src/plugins/guided_onboarding/server/feature.ts index e7f1ad2ce1274..41151421942f9 100644 --- a/src/plugins/guided_onboarding/server/feature.ts +++ b/src/plugins/guided_onboarding/server/feature.ts @@ -10,6 +10,7 @@ import type { KibanaFeatureConfig } from '@kbn/features-plugin/common'; import { i18n } from '@kbn/i18n'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { PLUGIN_FEATURE, PLUGIN_ID } from '../common/constants'; import { guideStateSavedObjectsType, pluginStateSavedObjectsType } from './saved_objects'; @@ -19,6 +20,7 @@ export const GUIDED_ONBOARDING_FEATURE: KibanaFeatureConfig = { defaultMessage: 'Setup guides', }), category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [PLUGIN_ID], privileges: { all: { diff --git a/x-pack/examples/alerting_example/server/plugin.ts b/x-pack/examples/alerting_example/server/plugin.ts index 110071a17fa2d..43b3bc82c4416 100644 --- a/x-pack/examples/alerting_example/server/plugin.ts +++ b/x-pack/examples/alerting_example/server/plugin.ts @@ -12,6 +12,7 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { PluginSetupContract as AlertingSetup } from '@kbn/alerting-plugin/server'; import { FeaturesPluginSetup } from '@kbn/features-plugin/server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { ruleType as alwaysFiringRule } from './rule_types/always_firing'; import { ruleType as peopleInSpaceRule } from './rule_types/astros'; import { ruleType as patternRule } from './rule_types/pattern'; @@ -41,6 +42,7 @@ export class AlertingExamplePlugin implements Plugin ({ ), order: 1100, category: DEFAULT_APP_CATEGORIES.security, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [ASSISTANT_FEATURE_ID, 'kibana'], catalogue: [APP_ID], minimumLicense: 'enterprise', diff --git a/x-pack/packages/security-solution/features/src/attack_discovery/kibana_features.ts b/x-pack/packages/security-solution/features/src/attack_discovery/kibana_features.ts index 130ee907b8e83..26f81b65213e0 100644 --- a/x-pack/packages/security-solution/features/src/attack_discovery/kibana_features.ts +++ b/x-pack/packages/security-solution/features/src/attack_discovery/kibana_features.ts @@ -7,6 +7,7 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { i18n } from '@kbn/i18n'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { APP_ID, ATTACK_DISCOVERY_FEATURE_ID } from '../constants'; import { type BaseKibanaFeatureConfig } from '../types'; @@ -21,6 +22,7 @@ export const getAttackDiscoveryBaseKibanaFeature = (): BaseKibanaFeatureConfig = ), order: 1100, category: DEFAULT_APP_CATEGORIES.security, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [ATTACK_DISCOVERY_FEATURE_ID, 'kibana'], catalogue: [APP_ID], minimumLicense: 'enterprise', diff --git a/x-pack/packages/security-solution/features/src/cases/kibana_features.ts b/x-pack/packages/security-solution/features/src/cases/kibana_features.ts index a8da25bb6e40b..dd49a60328288 100644 --- a/x-pack/packages/security-solution/features/src/cases/kibana_features.ts +++ b/x-pack/packages/security-solution/features/src/cases/kibana_features.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import type { BaseKibanaFeatureConfig } from '../types'; import { APP_ID, CASES_FEATURE_ID } from '../constants'; import type { CasesFeatureParams } from './types'; @@ -27,6 +28,7 @@ export const getCasesBaseKibanaFeature = ({ ), order: 1100, category: DEFAULT_APP_CATEGORIES.security, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [CASES_FEATURE_ID, 'kibana'], catalogue: [APP_ID], cases: [APP_ID], diff --git a/x-pack/packages/security-solution/features/src/security/kibana_features.ts b/x-pack/packages/security-solution/features/src/security/kibana_features.ts index 5ba6bf68b0e52..2fb6b11988797 100644 --- a/x-pack/packages/security-solution/features/src/security/kibana_features.ts +++ b/x-pack/packages/security-solution/features/src/security/kibana_features.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { @@ -52,6 +53,7 @@ export const getSecurityBaseKibanaFeature = ({ ), order: 1100, category: DEFAULT_APP_CATEGORIES.security, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [APP_ID, CLOUD_POSTURE_APP_ID, CLOUD_DEFEND_APP_ID, 'kibana'], catalogue: [APP_ID], management: { diff --git a/x-pack/plugins/actions/server/feature.ts b/x-pack/plugins/actions/server/feature.ts index 9fc48b705d25b..d4a9d3a3537bf 100644 --- a/x-pack/plugins/actions/server/feature.ts +++ b/x-pack/plugins/actions/server/feature.ts @@ -7,6 +7,7 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { i18n } from '@kbn/i18n'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, @@ -28,6 +29,7 @@ export const ACTIONS_FEATURE = { defaultMessage: 'Actions and Connectors', }), category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [], order: FEATURE_ORDER, management: { diff --git a/x-pack/plugins/alerting/server/maintenance_window_feature.ts b/x-pack/plugins/alerting/server/maintenance_window_feature.ts index a0bd117d47721..1ae24c1c45f09 100644 --- a/x-pack/plugins/alerting/server/maintenance_window_feature.ts +++ b/x-pack/plugins/alerting/server/maintenance_window_feature.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { KibanaFeatureConfig } from '@kbn/features-plugin/common'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { MAINTENANCE_WINDOW_FEATURE_ID, MAINTENANCE_WINDOW_API_PRIVILEGES, @@ -20,6 +21,7 @@ export const maintenanceWindowFeature: KibanaFeatureConfig = { defaultMessage: 'Maintenance Windows', }), category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [], management: { insightsAndAlerting: ['maintenanceWindows'], diff --git a/x-pack/plugins/alerting/server/rules_settings/rules_settings_feature.test.ts b/x-pack/plugins/alerting/server/rules_settings/rules_settings_feature.test.ts index cdf4f034bc528..9b5143cc8b76e 100644 --- a/x-pack/plugins/alerting/server/rules_settings/rules_settings_feature.test.ts +++ b/x-pack/plugins/alerting/server/rules_settings/rules_settings_feature.test.ts @@ -16,6 +16,7 @@ test('returns rule settings feature with query delay subfeature if serverless', label: 'Management', order: 5000, }, + scope: ['spaces', 'security'], id: 'rulesSettings', management: { insightsAndAlerting: ['triggersActions'], @@ -125,6 +126,7 @@ test('returns rule settings feature without query delay subfeature if not server label: 'Management', order: 5000, }, + scope: ['spaces', 'security'], id: 'rulesSettings', management: { insightsAndAlerting: ['triggersActions'], diff --git a/x-pack/plugins/alerting/server/rules_settings/rules_settings_feature.ts b/x-pack/plugins/alerting/server/rules_settings/rules_settings_feature.ts index de01cec619dff..57a4e333c0b56 100644 --- a/x-pack/plugins/alerting/server/rules_settings/rules_settings_feature.ts +++ b/x-pack/plugins/alerting/server/rules_settings/rules_settings_feature.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { KibanaFeatureConfig } from '@kbn/features-plugin/common'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { RULES_SETTINGS_FEATURE_ID, READ_FLAPPING_SETTINGS_SUB_FEATURE_ID, @@ -25,6 +26,7 @@ export function getRulesSettingsFeature(isServerless: boolean): KibanaFeatureCon defaultMessage: 'Rules Settings', }), category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [], management: { insightsAndAlerting: ['triggersActions'], diff --git a/x-pack/plugins/canvas/server/feature.test.ts b/x-pack/plugins/canvas/server/feature.test.ts index b4a891af13c6f..c145140f8d944 100644 --- a/x-pack/plugins/canvas/server/feature.test.ts +++ b/x-pack/plugins/canvas/server/feature.test.ts @@ -78,6 +78,10 @@ it('Provides a feature declaration ', () => { ], }, }, + "scope": Array [ + "spaces", + "security", + ], "subFeatures": Array [], } `); @@ -152,6 +156,10 @@ it(`Calls on Reporting whether to include Generate PDF as a sub-feature`, () => ], }, }, + "scope": Array [ + "spaces", + "security", + ], "subFeatures": Array [ Object { "name": "Reporting", diff --git a/x-pack/plugins/canvas/server/feature.ts b/x-pack/plugins/canvas/server/feature.ts index 01ae5249629e9..2406ac133c721 100644 --- a/x-pack/plugins/canvas/server/feature.ts +++ b/x-pack/plugins/canvas/server/feature.ts @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { KibanaFeatureConfig } from '@kbn/features-plugin/common'; import { ReportingStart } from '@kbn/reporting-plugin/server/types'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; /* * Register Canvas as a Kibana feature, @@ -22,6 +23,7 @@ export function getCanvasFeature(plugins: { reporting?: ReportingStart }): Kiban name: 'Canvas', order: 300, category: DEFAULT_APP_CATEGORIES.kibana, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: ['canvas', 'kibana'], management: { ...(includeReporting ? { insightsAndAlerting: ['reporting'] } : {}), diff --git a/x-pack/plugins/cases/server/features.ts b/x-pack/plugins/cases/server/features.ts index 9f8bbdabf0c8e..f8f162b2ae3dc 100644 --- a/x-pack/plugins/cases/server/features.ts +++ b/x-pack/plugins/cases/server/features.ts @@ -11,6 +11,7 @@ import type { KibanaFeatureConfig } from '@kbn/features-plugin/common'; import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { APP_ID, FEATURE_ID } from '../common/constants'; import { createUICapabilities, getApiTags } from '../common'; @@ -32,6 +33,7 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => { defaultMessage: 'Cases', }), category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [], order: FEATURE_ORDER, management: { diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 8c2563d1bbda9..c80216bc7a156 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -19,6 +19,7 @@ import { import { CustomIntegrationsPluginSetup } from '@kbn/custom-integrations-plugin/server'; import { DataPluginStart } from '@kbn/data-plugin/server/plugin'; import { ENTERPRISE_SEARCH_APP_ID } from '@kbn/deeplinks-search'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { FeaturesPluginSetup } from '@kbn/features-plugin/server'; import { GlobalSearchPluginSetup } from '@kbn/global-search-plugin/server'; import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server'; @@ -202,6 +203,7 @@ export class EnterpriseSearchPlugin implements Plugin { name: SEARCH_PRODUCT_NAME, order: 0, category: DEFAULT_APP_CATEGORIES.enterpriseSearch, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: ['kibana', ...PLUGIN_IDS], catalogue: PLUGIN_IDS, privileges: { diff --git a/x-pack/plugins/features/common/index.ts b/x-pack/plugins/features/common/index.ts index 92cbcd76172d0..6a4411f525083 100644 --- a/x-pack/plugins/features/common/index.ts +++ b/x-pack/plugins/features/common/index.ts @@ -10,7 +10,7 @@ export type { FeatureKibanaPrivileges } from './feature_kibana_privileges'; export type { ElasticsearchFeatureConfig } from './elasticsearch_feature'; export { ElasticsearchFeature } from './elasticsearch_feature'; export type { KibanaFeatureConfig } from './kibana_feature'; -export { KibanaFeature } from './kibana_feature'; +export { KibanaFeature, KibanaFeatureScope } from './kibana_feature'; export type { SubFeatureConfig, SubFeaturePrivilegeConfig, diff --git a/x-pack/plugins/features/common/kibana_feature.ts b/x-pack/plugins/features/common/kibana_feature.ts index 926aca01627a2..bafa0329d359d 100644 --- a/x-pack/plugins/features/common/kibana_feature.ts +++ b/x-pack/plugins/features/common/kibana_feature.ts @@ -12,6 +12,16 @@ import { FeatureKibanaPrivileges } from './feature_kibana_privileges'; import { SubFeatureConfig, SubFeature as KibanaSubFeature } from './sub_feature'; import { ReservedKibanaPrivilege } from './reserved_kibana_privilege'; +/** + * Enum for allowed feature scope values. + * security - The feature is available in Security Feature Privileges. + * spaces - The feature is available in the Spaces Visibility Toggles. + */ +export enum KibanaFeatureScope { + Security = 'security', + Spaces = 'spaces', +} + /** * Interface for registering a feature. * Feature registration allows plugins to hide their applications with spaces, @@ -149,6 +159,11 @@ export interface KibanaFeatureConfig { * are visible. */ hidden?: boolean; + + /** + * Indicates whether the feature is available in Security Feature Privileges and the Spaces Visibility Toggles. + */ + scope?: readonly KibanaFeatureScope[]; } export class KibanaFeature { @@ -220,6 +235,10 @@ export class KibanaFeature { return this.config.reserved; } + public get scope() { + return this.config.scope; + } + public toRaw() { return { ...this.config } as KibanaFeatureConfig; } diff --git a/x-pack/plugins/features/public/index.ts b/x-pack/plugins/features/public/index.ts index c5100722795bc..e5e0e3bea51f9 100644 --- a/x-pack/plugins/features/public/index.ts +++ b/x-pack/plugins/features/public/index.ts @@ -14,7 +14,7 @@ export type { SubFeatureConfig, SubFeaturePrivilegeConfig, } from '../common'; -export { KibanaFeature } from '../common'; +export { KibanaFeature, KibanaFeatureScope } from '../common'; export type { FeaturesPluginSetup, FeaturesPluginStart } from './plugin'; diff --git a/x-pack/plugins/features/server/feature_registry.test.ts b/x-pack/plugins/features/server/feature_registry.test.ts index f27c93ac9129e..d9451fec632d8 100644 --- a/x-pack/plugins/features/server/feature_registry.test.ts +++ b/x-pack/plugins/features/server/feature_registry.test.ts @@ -200,6 +200,25 @@ describe('FeatureRegistry', () => { }); }); + it('requires only a valid scope registered', () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + category: { id: 'foo', label: 'foo' }, + privileges: null, + // @ts-expect-error + scope: ['foo', 'bar'], + }; + + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature has unknown scope entries: foo, bar"` + ); + }); + it(`requires a value for privileges`, () => { const feature: KibanaFeatureConfig = { id: 'test-feature', diff --git a/x-pack/plugins/features/server/feature_registry.ts b/x-pack/plugins/features/server/feature_registry.ts index 4726335ee3d01..686a3f7d5c31d 100644 --- a/x-pack/plugins/features/server/feature_registry.ts +++ b/x-pack/plugins/features/server/feature_registry.ts @@ -15,6 +15,7 @@ import { ElasticsearchFeatureConfig, ElasticsearchFeature, SubFeaturePrivilegeConfig, + KibanaFeatureScope, } from '../common'; import { validateKibanaFeature, validateElasticsearchFeature } from './feature_schema'; import type { ConfigOverridesType } from './config'; @@ -41,6 +42,10 @@ export class FeatureRegistry { throw new Error(`Feature with id ${feature.id} is already registered.`); } + if (!feature.scope) { + feature.scope = [KibanaFeatureScope.Security]; + } + const featureCopy = cloneDeep(feature); this.kibanaFeatures[feature.id] = applyAutomaticPrivilegeGrants(featureCopy); diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 341bb926b277d..bd60eaa84f51c 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { difference } from 'lodash'; import { Capabilities as UICapabilities } from '@kbn/core/server'; -import { KibanaFeatureConfig } from '../common'; +import { KibanaFeatureConfig, KibanaFeatureScope } from '../common'; import { FeatureKibanaPrivileges, ElasticsearchFeatureConfig } from '.'; // Each feature gets its own property on the UICapabilities object, @@ -202,6 +202,7 @@ const kibanaFeatureSchema = schema.object({ }), name: schema.string(), category: appCategorySchema, + scope: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })), description: schema.maybe(schema.string()), order: schema.maybe(schema.number()), excludeFromBasePrivileges: schema.maybe(schema.boolean()), @@ -211,13 +212,23 @@ const kibanaFeatureSchema = schema.object({ catalogue: schema.maybe(catalogueSchema), alerting: schema.maybe(alertingSchema), cases: schema.maybe(casesSchema), - privileges: schema.oneOf([ - schema.literal(null), - schema.object({ - all: schema.maybe(kibanaPrivilegeSchema), - read: schema.maybe(kibanaPrivilegeSchema), + // Features registered only for the spaces scope should not have a `privileges` property. + // Such features are applicable only to the Spaces Visibility Toggles + privileges: schema.conditional( + schema.siblingRef('scope'), + schema.arrayOf(schema.literal('spaces'), { + minSize: 1, + maxSize: 1, }), - ]), + schema.literal(null), + schema.oneOf([ + schema.literal(null), + schema.object({ + all: schema.maybe(kibanaPrivilegeSchema), + read: schema.maybe(kibanaPrivilegeSchema), + }), + ]) + ), subFeatures: schema.maybe( schema.conditional( schema.siblingRef('privileges'), @@ -275,6 +286,14 @@ const elasticsearchFeatureSchema = schema.object({ export function validateKibanaFeature(feature: KibanaFeatureConfig) { kibanaFeatureSchema.validate(feature); + const unknownScopesEntries = difference(feature.scope ?? [], Object.values(KibanaFeatureScope)); + + if (unknownScopesEntries.length) { + throw new Error( + `Feature ${feature.id} has unknown scope entries: ${unknownScopesEntries.join(', ')}` + ); + } + // the following validation can't be enforced by the Joi schema, since it'd require us looking "up" the object graph for the list of valid value, which they explicitly forbid. const { app = [], management = {}, catalogue = [], alerting = [], cases = [] } = feature; diff --git a/x-pack/plugins/features/server/oss_features.ts b/x-pack/plugins/features/server/oss_features.ts index 90c997352e2ba..abc66ea61b199 100644 --- a/x-pack/plugins/features/server/oss_features.ts +++ b/x-pack/plugins/features/server/oss_features.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; +import { KibanaFeatureScope } from '../common'; import type { KibanaFeatureConfig, SubFeatureConfig } from '../common'; export interface BuildOSSFeaturesParams { @@ -30,6 +31,7 @@ export const buildOSSFeatures = ({ }, order: 100, category: DEFAULT_APP_CATEGORIES.kibana, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: ['discover', 'kibana'], catalogue: ['discover'], privileges: { @@ -125,6 +127,7 @@ export const buildOSSFeatures = ({ }, order: 700, category: DEFAULT_APP_CATEGORIES.kibana, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: ['visualize', 'lens', 'kibana'], catalogue: ['visualize'], privileges: { @@ -189,6 +192,7 @@ export const buildOSSFeatures = ({ }, order: 200, category: DEFAULT_APP_CATEGORIES.kibana, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: ['dashboards', 'kibana'], catalogue: ['dashboard'], privileges: { @@ -302,6 +306,7 @@ export const buildOSSFeatures = ({ }), order: 1300, category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: ['dev_tools', 'kibana'], catalogue: ['console', 'searchprofiler', 'grokdebugger'], privileges: { @@ -338,6 +343,7 @@ export const buildOSSFeatures = ({ }), order: 1500, category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: ['kibana'], catalogue: ['advanced_settings'], management: { @@ -377,6 +383,7 @@ export const buildOSSFeatures = ({ }), order: 1600, category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: ['kibana'], catalogue: ['indexPatterns'], management: { @@ -416,6 +423,7 @@ export const buildOSSFeatures = ({ }), order: 1600, category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: ['kibana'], catalogue: [], management: { @@ -455,6 +463,7 @@ export const buildOSSFeatures = ({ }), order: 1600, category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: ['kibana'], catalogue: [], privilegesTooltip: i18n.translate('xpack.features.filesSharedImagesPrivilegesTooltip', { @@ -488,6 +497,7 @@ export const buildOSSFeatures = ({ }), order: 1700, category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: ['kibana'], catalogue: ['saved_objects'], management: { @@ -529,6 +539,7 @@ export const buildOSSFeatures = ({ }), order: 1750, category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: ['kibana'], catalogue: [], privilegesTooltip: i18n.translate('xpack.features.savedQueryManagementTooltip', { diff --git a/x-pack/plugins/features/server/plugin.test.ts b/x-pack/plugins/features/server/plugin.test.ts index d353ee0588d5f..96062085e577e 100644 --- a/x-pack/plugins/features/server/plugin.test.ts +++ b/x-pack/plugins/features/server/plugin.test.ts @@ -188,6 +188,9 @@ describe('Features Plugin', () => { "ui": Array [], }, }, + "scope": Array [ + "security", + ], }, "subFeatures": Array [], } diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index f292900e92b72..21c3f1bf97f12 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -55,6 +55,7 @@ import type { SpacesPluginStart } from '@kbn/spaces-plugin/server'; import type { SavedObjectTaggingStart } from '@kbn/saved-objects-tagging-plugin/server'; import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import type { FleetConfigType } from '../common/types'; import type { FleetAuthz } from '../common'; @@ -322,6 +323,7 @@ export class FleetPlugin id: `fleetv2`, name: 'Fleet', category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [PLUGIN_ID], catalogue: ['fleet'], privilegesTooltip: i18n.translate('xpack.fleet.serverPlugin.privilegesTooltip', { @@ -484,6 +486,7 @@ export class FleetPlugin id: 'fleet', // for BWC name: 'Integrations', category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [INTEGRATIONS_PLUGIN_ID], catalogue: ['fleet'], privileges: { diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts index 27d3577e047a2..aa794fcd98a55 100644 --- a/x-pack/plugins/graph/server/plugin.ts +++ b/x-pack/plugins/graph/server/plugin.ts @@ -12,6 +12,7 @@ import { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugi import { HomeServerPluginSetup } from '@kbn/home-plugin/server'; import { FeaturesPluginSetup } from '@kbn/features-plugin/server'; import { ContentManagementServerSetup } from '@kbn/content-management-plugin/server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { LicenseState } from './lib/license_state'; import { registerSearchRoute } from './routes/search'; import { registerExploreRoute } from './routes/explore'; @@ -68,6 +69,7 @@ export class GraphPlugin implements Plugin { }), order: 600, category: DEFAULT_APP_CATEGORIES.kibana, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: ['graph', 'kibana'], catalogue: ['graph'], minimumLicense: 'platinum', diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 2366731c3826e..1b98310f798e4 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -18,6 +18,7 @@ import { HomeServerPluginSetup } from '@kbn/home-plugin/server'; import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common'; import type { EMSSettings } from '@kbn/maps-ems-plugin/server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { CONTENT_ID, LATEST_VERSION } from '../common/content_management'; import { getEcommerceSavedObjects } from './sample_data/ecommerce_saved_objects'; import { getFlightsSavedObjects } from './sample_data/flights_saved_objects'; @@ -175,6 +176,7 @@ export class MapsPlugin implements Plugin { }), order: 400, category: DEFAULT_APP_CATEGORIES.kibana, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [APP_ID, 'kibana'], catalogue: [APP_ID], privileges: { diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index ac3fc8caea569..2a36e94f2f2e7 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -25,6 +25,7 @@ import type { SpacesPluginSetup } from '@kbn/spaces-plugin/server'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/server'; import type { HomeServerPluginSetup } from '@kbn/home-plugin/server'; import type { CasesServerSetup } from '@kbn/cases-plugin/server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import type { PluginsSetup, PluginsStart, RouteInitialization } from './types'; import type { MlCapabilities } from '../common/types/capabilities'; import { notificationsRoutes } from './routes/notifications'; @@ -129,6 +130,7 @@ export class MlServerPlugin }), order: 500, category: DEFAULT_APP_CATEGORIES.kibana, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [PLUGIN_ID, 'kibana'], catalogue: [PLUGIN_ID, `${PLUGIN_ID}_file_data_visualizer`], privilegesTooltip: i18n.translate('xpack.ml.featureRegistry.privilegesTooltip', { diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 5219c3441b13e..72063805b3383 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -23,6 +23,7 @@ import { import { get } from 'lodash'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { RouteMethod } from '@kbn/core/server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { KIBANA_MONITORING_LOGGING_TAG, KIBANA_STATS_TYPE_MONITORING, @@ -272,6 +273,7 @@ export class MonitoringPlugin defaultMessage: 'Stack Monitoring', }), category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: ['monitoring', 'kibana'], catalogue: ['monitoring'], privileges: null, diff --git a/x-pack/plugins/observability_solution/apm/server/feature.ts b/x-pack/plugins/observability_solution/apm/server/feature.ts index 5a0ba552a0c18..1932a07b5ebd6 100644 --- a/x-pack/plugins/observability_solution/apm/server/feature.ts +++ b/x-pack/plugins/observability_solution/apm/server/feature.ts @@ -15,6 +15,7 @@ import { import { APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE } from '@kbn/apm-data-access-plugin/server/saved_objects/apm_indices'; import { ApmRuleType } from '@kbn/rule-data-utils'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { APM_SERVER_FEATURE_ID } from '../common/rules/apm_rule_types'; const ruleTypes = Object.values(ApmRuleType); @@ -26,6 +27,7 @@ export const APM_FEATURE = { }), order: 900, category: DEFAULT_APP_CATEGORIES.observability, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [APM_SERVER_FEATURE_ID, 'ux', 'kibana'], catalogue: [APM_SERVER_FEATURE_ID], management: { diff --git a/x-pack/plugins/observability_solution/infra/server/features.ts b/x-pack/plugins/observability_solution/infra/server/features.ts index 6d209833fcd77..48091c9fe4b7f 100644 --- a/x-pack/plugins/observability_solution/infra/server/features.ts +++ b/x-pack/plugins/observability_solution/infra/server/features.ts @@ -14,6 +14,7 @@ import { } from '@kbn/rule-data-utils'; import { ES_QUERY_ID } from '@kbn/rule-data-utils'; import { metricsDataSourceSavedObjectName } from '@kbn/metrics-data-access-plugin/server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { LOG_DOCUMENT_COUNT_RULE_TYPE_ID } from '../common/alerting/logs/log_threshold/types'; import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, @@ -37,6 +38,7 @@ export const METRICS_FEATURE = { }), order: 800, category: DEFAULT_APP_CATEGORIES.observability, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: ['infra', 'metrics', 'kibana'], catalogue: ['infraops', 'metrics'], management: { @@ -103,6 +105,7 @@ export const LOGS_FEATURE = { }), order: 700, category: DEFAULT_APP_CATEGORIES.observability, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: ['infra', 'logs', 'kibana'], catalogue: ['infralogging', 'logs'], management: { diff --git a/x-pack/plugins/observability_solution/observability/server/plugin.ts b/x-pack/plugins/observability_solution/observability/server/plugin.ts index 4b04ac032a82c..6bc21bf5dddf2 100644 --- a/x-pack/plugins/observability_solution/observability/server/plugin.ts +++ b/x-pack/plugins/observability_solution/observability/server/plugin.ts @@ -37,6 +37,7 @@ import { SharePluginSetup } from '@kbn/share-plugin/server'; import { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { ObservabilityConfig } from '.'; import { casesFeatureId, observabilityFeatureId } from '../common'; import { @@ -112,6 +113,7 @@ export class ObservabilityPlugin implements Plugin { }), order: 1100, category: DEFAULT_APP_CATEGORIES.observability, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [casesFeatureId, 'kibana'], catalogue: [observabilityFeatureId], cases: [observabilityFeatureId], @@ -235,6 +237,7 @@ export class ObservabilityPlugin implements Plugin { }), order: 1000, category: DEFAULT_APP_CATEGORIES.observability, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [observabilityFeatureId], catalogue: [observabilityFeatureId], alerting: o11yRuleTypes, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts index 8a6f414ec92de..50687920478af 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts @@ -20,6 +20,7 @@ import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, } from '@kbn/actions-plugin/server/constants/saved_objects'; import { firstValueFrom } from 'rxjs'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { OBSERVABILITY_AI_ASSISTANT_FEATURE_ID } from '../common/feature'; import type { ObservabilityAIAssistantConfig } from './config'; import { registerServerRoutes } from './routes/register_routes'; @@ -69,6 +70,7 @@ export class ObservabilityAIAssistantPlugin }), order: 8600, category: DEFAULT_APP_CATEGORIES.observability, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [OBSERVABILITY_AI_ASSISTANT_FEATURE_ID, 'kibana'], catalogue: [OBSERVABILITY_AI_ASSISTANT_FEATURE_ID], minimumLicense: 'enterprise', diff --git a/x-pack/plugins/observability_solution/profiling/server/feature.ts b/x-pack/plugins/observability_solution/profiling/server/feature.ts index 13e064364b7b8..039c6b3af37a1 100644 --- a/x-pack/plugins/observability_solution/profiling/server/feature.ts +++ b/x-pack/plugins/observability_solution/profiling/server/feature.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; export const PROFILING_SERVER_FEATURE_ID = 'profiling'; @@ -17,6 +18,7 @@ export const PROFILING_FEATURE = { }), order: 1200, category: DEFAULT_APP_CATEGORIES.observability, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [PROFILING_SERVER_FEATURE_ID, 'ux', 'kibana'], // see x-pack/plugins/features/common/feature_kibana_privileges.ts privileges: { diff --git a/x-pack/plugins/observability_solution/slo/server/plugin.ts b/x-pack/plugins/observability_solution/slo/server/plugin.ts index 76e20c45630ad..a2e4229b2b952 100644 --- a/x-pack/plugins/observability_solution/slo/server/plugin.ts +++ b/x-pack/plugins/observability_solution/slo/server/plugin.ts @@ -33,6 +33,7 @@ import { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server' import { AlertsLocatorDefinition } from '@kbn/observability-plugin/common'; import { SLO_BURN_RATE_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { sloFeatureId } from '@kbn/observability-plugin/common'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { registerSloUsageCollector } from './lib/collectors/register'; import { SloOrphanSummaryCleanupTask } from './services/tasks/orphan_summary_cleanup_task'; import { slo, SO_SLO_TYPE } from './saved_objects'; @@ -88,6 +89,7 @@ export class SloPlugin implements Plugin { }), order: 1200, category: DEFAULT_APP_CATEGORIES.observability, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [sloFeatureId, 'kibana'], catalogue: [sloFeatureId, 'observability'], alerting: sloRuleTypes, diff --git a/x-pack/plugins/observability_solution/synthetics/server/feature.ts b/x-pack/plugins/observability_solution/synthetics/server/feature.ts index b3290936441b9..c8b4b721a9ce1 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/feature.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/feature.ts @@ -11,6 +11,7 @@ import { SubFeaturePrivilegeGroupConfig, SubFeaturePrivilegeGroupType, } from '@kbn/features-plugin/common'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { syntheticsMonitorType, syntheticsParamType } from '../common/types/saved_objects'; import { SYNTHETICS_RULE_TYPES } from '../common/constants/synthetics_alerts'; import { privateLocationsSavedObjectName } from '../common/saved_objects/private_locations'; @@ -55,6 +56,7 @@ export const syntheticsFeature = { category: DEFAULT_APP_CATEGORIES.observability, app: ['uptime', 'kibana', 'synthetics'], catalogue: ['uptime'], + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], management: { insightsAndAlerting: ['triggersActions'], }, diff --git a/x-pack/plugins/osquery/server/utils/register_features.ts b/x-pack/plugins/osquery/server/utils/register_features.ts index 5af2335489dff..46db233a6dce4 100644 --- a/x-pack/plugins/osquery/server/utils/register_features.ts +++ b/x-pack/plugins/osquery/server/utils/register_features.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { packSavedObjectType, packAssetSavedObjectType, @@ -22,6 +23,7 @@ export const registerFeatures = (features: SetupPlugins['features']) => { defaultMessage: 'Osquery', }), category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [PLUGIN_ID, 'kibana'], catalogue: [PLUGIN_ID], order: 2300, diff --git a/x-pack/plugins/reporting/server/features.ts b/x-pack/plugins/reporting/server/features.ts index 7c87f19ca011a..5fb03f7428b26 100644 --- a/x-pack/plugins/reporting/server/features.ts +++ b/x-pack/plugins/reporting/server/features.ts @@ -8,6 +8,7 @@ import { DEFAULT_APP_CATEGORIES, type Logger } from '@kbn/core/server'; import { i18n } from '@kbn/i18n'; import type { FeaturesPluginSetup } from '@kbn/features-plugin/server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; interface FeatureRegistrationOpts { features: FeaturesPluginSetup; @@ -37,6 +38,7 @@ export function registerFeatures({ defaultMessage: 'Reporting', }), category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [], privileges: { all: { savedObject: { all: [], read: [] }, ui: [] }, diff --git a/x-pack/plugins/reporting/server/plugin.test.ts b/x-pack/plugins/reporting/server/plugin.test.ts index de576fe95d006..2322794836989 100644 --- a/x-pack/plugins/reporting/server/plugin.test.ts +++ b/x-pack/plugins/reporting/server/plugin.test.ts @@ -186,6 +186,7 @@ describe('Reporting Plugin', () => { id: 'reporting', name: 'Reporting', category: DEFAULT_APP_CATEGORIES.management, + scope: ['spaces', 'security'], app: [], privileges: { all: { savedObject: { all: [], read: [] }, ui: [] }, diff --git a/x-pack/plugins/saved_objects_tagging/server/features.ts b/x-pack/plugins/saved_objects_tagging/server/features.ts index aaaf030f2c27e..30c5fc7b910f6 100644 --- a/x-pack/plugins/saved_objects_tagging/server/features.ts +++ b/x-pack/plugins/saved_objects_tagging/server/features.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { KibanaFeatureConfig } from '@kbn/features-plugin/server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { tagSavedObjectTypeName, tagManagementSectionId, tagFeatureId } from '../common/constants'; export const savedObjectsTaggingFeature: KibanaFeatureConfig = { @@ -16,6 +17,7 @@ export const savedObjectsTaggingFeature: KibanaFeatureConfig = { defaultMessage: 'Tag Management', }), category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], order: 1800, app: [], management: { diff --git a/x-pack/plugins/search_inference_endpoints/server/plugin.ts b/x-pack/plugins/search_inference_endpoints/server/plugin.ts index b96bd0f9da55b..425820495e997 100644 --- a/x-pack/plugins/search_inference_endpoints/server/plugin.ts +++ b/x-pack/plugins/search_inference_endpoints/server/plugin.ts @@ -13,6 +13,7 @@ import { Plugin, PluginInitializerContext, } from '@kbn/core/server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { defineRoutes } from './routes'; import { SearchInferenceEndpointsPluginSetup, @@ -56,6 +57,7 @@ export class SearchInferenceEndpointsPlugin order: 0, category: DEFAULT_APP_CATEGORIES.enterpriseSearch, app: ['kibana', PLUGIN_ID], + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], catalogue: [PLUGIN_ID], privileges: { all: { diff --git a/x-pack/plugins/spaces/public/management/components/enabled_features/__snapshots__/enabled_features.test.tsx.snap b/x-pack/plugins/spaces/public/management/components/enabled_features/__snapshots__/enabled_features.test.tsx.snap index 4bf010004cbef..fd56cf65620f1 100644 --- a/x-pack/plugins/spaces/public/management/components/enabled_features/__snapshots__/enabled_features.test.tsx.snap +++ b/x-pack/plugins/spaces/public/management/components/enabled_features/__snapshots__/enabled_features.test.tsx.snap @@ -55,6 +55,10 @@ exports[`EnabledFeatures renders as expected 1`] = ` "id": "feature-1", "name": "Feature 1", "privileges": null, + "scope": Array [ + "spaces", + "security", + ], }, Object { "app": Array [], @@ -67,6 +71,10 @@ exports[`EnabledFeatures renders as expected 1`] = ` "id": "feature-2", "name": "Feature 2", "privileges": null, + "scope": Array [ + "spaces", + "security", + ], }, ] } diff --git a/x-pack/plugins/spaces/public/management/components/enabled_features/enabled_features.test.tsx b/x-pack/plugins/spaces/public/management/components/enabled_features/enabled_features.test.tsx index 27e5dfa8d4137..d8d3d76f6858e 100644 --- a/x-pack/plugins/spaces/public/management/components/enabled_features/enabled_features.test.tsx +++ b/x-pack/plugins/spaces/public/management/components/enabled_features/enabled_features.test.tsx @@ -9,6 +9,7 @@ import type { EuiCheckboxProps } from '@elastic/eui'; import React from 'react'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core/public'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import type { KibanaFeatureConfig } from '@kbn/features-plugin/public'; import { findTestSubject, mountWithIntl, nextTick, shallowWithIntl } from '@kbn/test-jest-helpers'; @@ -18,6 +19,7 @@ const features: KibanaFeatureConfig[] = [ { id: 'feature-1', name: 'Feature 1', + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [], category: DEFAULT_APP_CATEGORIES.kibana, privileges: null, @@ -25,6 +27,7 @@ const features: KibanaFeatureConfig[] = [ { id: 'feature-2', name: 'Feature 2', + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: [], category: DEFAULT_APP_CATEGORIES.kibana, privileges: null, diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts index 31df41beae3cf..d48095638babf 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts @@ -27,6 +27,7 @@ const features = [ id: 'feature_2', name: 'Feature 2', app: ['feature2'], + scope: ['spaces', 'security'], catalogue: ['feature2Entry'], management: { kibana: ['somethingElse'], @@ -47,6 +48,7 @@ const features = [ id: 'feature_3', name: 'Feature 3', app: ['feature3_app'], + scope: ['spaces', 'security'], catalogue: ['feature3Entry'], management: { kibana: ['indices'], @@ -68,6 +70,7 @@ const features = [ id: 'feature_4', name: 'Feature 4', app: ['feature3', 'feature3_app'], + scope: ['spaces', 'security'], catalogue: ['feature3Entry'], management: { kibana: ['indices'], diff --git a/x-pack/plugins/spaces/server/plugin.test.ts b/x-pack/plugins/spaces/server/plugin.test.ts index 1ca1f600d8bad..40aaf7044a4ea 100644 --- a/x-pack/plugins/spaces/server/plugin.test.ts +++ b/x-pack/plugins/spaces/server/plugin.test.ts @@ -117,7 +117,7 @@ describe('Spaces plugin', () => { const coreStart = coreMock.createStart(); - const spacesStart = plugin.start(coreStart); + const spacesStart = plugin.start(coreStart, { features: featuresPluginMock.createStart() }); expect(spacesStart).toMatchInlineSnapshot(` Object { "hasOnlyDefaultSpace$": Observable { @@ -154,7 +154,7 @@ describe('Spaces plugin', () => { const spacesSetup = plugin.setup(core, { features, licensing, usageCollection }); const coreStart = coreMock.createStart(); - const spacesStart = plugin.start(coreStart); + const spacesStart = plugin.start(coreStart, { features: featuresPluginMock.createStart() }); await expect(firstValueFrom(spacesSetup.hasOnlyDefaultSpace$)).resolves.toEqual(true); await expect(firstValueFrom(spacesStart.hasOnlyDefaultSpace$)).resolves.toEqual(true); @@ -172,7 +172,7 @@ describe('Spaces plugin', () => { const spacesSetup = plugin.setup(core, { features, licensing, usageCollection }); const coreStart = coreMock.createStart(); - const spacesStart = plugin.start(coreStart); + const spacesStart = plugin.start(coreStart, { features: featuresPluginMock.createStart() }); await expect(firstValueFrom(spacesSetup.hasOnlyDefaultSpace$)).resolves.toEqual(false); await expect(firstValueFrom(spacesStart.hasOnlyDefaultSpace$)).resolves.toEqual(false); diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index 24e2eac359de2..2f8fb2ec30842 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -235,8 +235,8 @@ export class SpacesPlugin }; } - public start(core: CoreStart) { - const spacesClientStart = this.spacesClientService.start(core); + public start(core: CoreStart, plugins: PluginsStart) { + const spacesClientStart = this.spacesClientService.start(core, plugins.features); this.spacesServiceStart = this.spacesService.start({ basePath: core.http.basePath, diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index 8c5782aacd519..b3363a423cf12 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -17,6 +17,7 @@ import { loggingSystemMock, } from '@kbn/core/server/mocks'; import { SPACES_EXTENSION_ID } from '@kbn/core-saved-objects-server'; +import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; import { initCopyToSpacesApi } from './copy_to_space'; import { spacesConfig } from '../../../lib/__fixtures__'; @@ -72,7 +73,7 @@ describe('copy to space', () => { usageStatsServiceMock.createSetupContract(usageStatsClient) ); - const clientServiceStart = clientService.start(coreStart); + const clientServiceStart = clientService.start(coreStart, featuresPluginMock.createStart()); const spacesServiceStart = service.start({ basePath: coreStart.http.basePath, diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts index 99ec34917eb5a..f50b73d7f8513 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts @@ -16,6 +16,7 @@ import { httpServiceMock, loggingSystemMock, } from '@kbn/core/server/mocks'; +import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; import { initDeleteSpacesApi } from './delete'; import { spacesConfig } from '../../../lib/__fixtures__'; @@ -54,7 +55,7 @@ describe('Spaces Public API', () => { const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract()); - const clientServiceStart = clientService.start(coreStart); + const clientServiceStart = clientService.start(coreStart, featuresPluginMock.createStart()); const spacesServiceStart = service.start({ basePath: coreStart.http.basePath, diff --git a/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.test.ts b/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.test.ts index e6f665f817c55..1192eef37fa33 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.test.ts @@ -15,6 +15,7 @@ import { httpServiceMock, loggingSystemMock, } from '@kbn/core/server/mocks'; +import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; import { initDisableLegacyUrlAliasesApi } from './disable_legacy_url_aliases'; import { spacesConfig } from '../../../lib/__fixtures__'; @@ -57,7 +58,7 @@ describe('_disable_legacy_url_aliases', () => { usageStatsServiceMock.createSetupContract(usageStatsClient) ); - const clientServiceStart = clientService.start(coreStart); + const clientServiceStart = clientService.start(coreStart, featuresPluginMock.createStart()); const spacesServiceStart = service.start({ basePath: coreStart.http.basePath, diff --git a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts index 938af432b0cfa..3b5774284f19f 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts @@ -14,6 +14,7 @@ import { httpServiceMock, loggingSystemMock, } from '@kbn/core/server/mocks'; +import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; import { initGetSpaceApi } from './get'; import { spacesConfig } from '../../../lib/__fixtures__'; @@ -53,7 +54,7 @@ describe('GET space', () => { const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract()); - const clientServiceStart = clientService.start(coreStart); + const clientServiceStart = clientService.start(coreStart, featuresPluginMock.createStart()); const spacesServiceStart = service.start({ basePath: coreStart.http.basePath, diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts index a3d4f151c6615..d2f8162a3f236 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts @@ -16,6 +16,7 @@ import { loggingSystemMock, } from '@kbn/core/server/mocks'; import { getRequestValidation } from '@kbn/core-http-server'; +import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; import { initGetAllSpacesApi } from './get_all'; import { spacesConfig } from '../../../lib/__fixtures__'; @@ -55,7 +56,7 @@ describe('GET /spaces/space', () => { const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract()); - const clientServiceStart = clientService.start(coreStart); + const clientServiceStart = clientService.start(coreStart, featuresPluginMock.createStart()); const spacesServiceStart = service.start({ basePath: coreStart.http.basePath, diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.test.ts index 8c9c60f78f461..ce4fce7bb822e 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.test.ts @@ -15,6 +15,7 @@ import { httpServiceMock, loggingSystemMock, } from '@kbn/core/server/mocks'; +import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; import { initGetShareableReferencesApi } from './get_shareable_references'; import { spacesConfig } from '../../../lib/__fixtures__'; @@ -54,7 +55,7 @@ describe('get shareable references', () => { const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract()); - const clientServiceStart = clientService.start(coreStart); + const clientServiceStart = clientService.start(coreStart, featuresPluginMock.createStart()); const spacesServiceStart = service.start({ basePath: coreStart.http.basePath, diff --git a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts index b36a3619a48e0..9b017839fb25c 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts @@ -16,6 +16,7 @@ import { httpServiceMock, loggingSystemMock, } from '@kbn/core/server/mocks'; +import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; import { initPostSpacesApi } from './post'; import { spacesConfig } from '../../../lib/__fixtures__'; @@ -52,9 +53,13 @@ describe('Spaces Public API', () => { basePath: httpService.basePath, }); + const featuresPluginMockStart = featuresPluginMock.createStart(); + + featuresPluginMockStart.getKibanaFeatures.mockReturnValue([]); + const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract()); - const clientServiceStart = clientService.start(coreStart); + const clientServiceStart = clientService.start(coreStart, featuresPluginMockStart); const spacesServiceStart = service.start({ basePath: coreStart.http.basePath, diff --git a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts index 2141e369507b9..fd255a8aadc2b 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts @@ -16,6 +16,7 @@ import { httpServiceMock, loggingSystemMock, } from '@kbn/core/server/mocks'; +import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; import { initPutSpacesApi } from './put'; import { spacesConfig } from '../../../lib/__fixtures__'; @@ -52,9 +53,13 @@ describe('PUT /api/spaces/space', () => { basePath: httpService.basePath, }); + const featuresPluginMockStart = featuresPluginMock.createStart(); + + featuresPluginMockStart.getKibanaFeatures.mockReturnValue([]); + const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract()); - const clientServiceStart = clientService.start(coreStart); + const clientServiceStart = clientService.start(coreStart, featuresPluginMockStart); const spacesServiceStart = service.start({ basePath: coreStart.http.basePath, diff --git a/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.test.ts b/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.test.ts index 874b5d4284958..6ae4e44582677 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.test.ts @@ -16,6 +16,7 @@ import { httpServiceMock, loggingSystemMock, } from '@kbn/core/server/mocks'; +import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; import { initUpdateObjectsSpacesApi } from './update_objects_spaces'; import { spacesConfig } from '../../../lib/__fixtures__'; @@ -55,7 +56,7 @@ describe('update_objects_spaces', () => { const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract()); - const clientServiceStart = clientService.start(coreStart); + const clientServiceStart = clientService.start(coreStart, featuresPluginMock.createStart()); const spacesServiceStart = service.start({ basePath: coreStart.http.basePath, diff --git a/x-pack/plugins/spaces/server/routes/api/internal/get_content_summary.test.ts b/x-pack/plugins/spaces/server/routes/api/internal/get_content_summary.test.ts index 09fdbe7896761..3de451ddfa730 100644 --- a/x-pack/plugins/spaces/server/routes/api/internal/get_content_summary.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/internal/get_content_summary.test.ts @@ -17,6 +17,7 @@ import { savedObjectsClientMock, savedObjectsTypeRegistryMock, } from '@kbn/core/server/mocks'; +import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; import type { SpaceContentTypeSummaryItem } from './get_content_summary'; import { initGetSpaceContentSummaryApi } from './get_content_summary'; @@ -81,7 +82,7 @@ describe('GET /internal/spaces/{spaceId}/content_summary', () => { basePath: httpService.basePath, }); - const clientServiceStart = clientService.start(coreStart); + const clientServiceStart = clientService.start(coreStart, featuresPluginMock.createStart()); const spacesServiceStart = service.start({ basePath: coreStart.http.basePath, diff --git a/x-pack/plugins/spaces/server/routes/api/internal/set_solution_space.test.ts b/x-pack/plugins/spaces/server/routes/api/internal/set_solution_space.test.ts index 181309d1ea731..6b1127db821b1 100644 --- a/x-pack/plugins/spaces/server/routes/api/internal/set_solution_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/internal/set_solution_space.test.ts @@ -10,6 +10,7 @@ import * as Rx from 'rxjs'; import type { RouteValidatorConfig } from '@kbn/core/server'; import { kibanaResponseFactory } from '@kbn/core/server'; import { coreMock, httpServerMock, httpServiceMock } from '@kbn/core/server/mocks'; +import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; import { initSetSolutionSpaceApi } from './set_solution_space'; import { spacesConfig } from '../../../lib/__fixtures__'; @@ -43,7 +44,7 @@ describe('PUT /internal/spaces/space/{id}/solution', () => { basePath: httpService.basePath, }); - const clientServiceStart = clientService.start(coreStart); + const clientServiceStart = clientService.start(coreStart, featuresPluginMock.createStart()); const spacesServiceStart = service.start({ basePath: coreStart.http.basePath, diff --git a/x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts b/x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts index 7faff9f8a0acf..f53bb02787f8d 100644 --- a/x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts +++ b/x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts @@ -7,6 +7,9 @@ import { savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; import type { SavedObject } from '@kbn/core-saved-objects-server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; +import { KibanaFeature } from '@kbn/features-plugin/server'; +import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; import { SpacesClient } from './spaces_client'; import type { GetAllSpacesPurpose, Space } from '../../common'; @@ -113,7 +116,8 @@ describe('#getAll', () => { mockConfig, mockCallWithRequestRepository, [], - 'traditional' + 'traditional', + featuresPluginMock.createStart() ); const actualSpaces = await client.getAll(); @@ -140,7 +144,8 @@ describe('#getAll', () => { mockConfig, mockCallWithRequestRepository, [], - 'serverless' + 'serverless', + featuresPluginMock.createStart() ); const [actualSpace] = await client.getAll(); const [{ solution, ...expectedSpace }] = expectedSpaces; @@ -164,7 +169,8 @@ describe('#getAll', () => { mockConfig, mockCallWithRequestRepository, [], - 'traditional' + 'traditional', + featuresPluginMock.createStart() ); await expect( client.getAll({ purpose: 'invalid_purpose' as GetAllSpacesPurpose }) @@ -211,7 +217,8 @@ describe('#get', () => { mockConfig, mockCallWithRequestRepository, [], - 'traditional' + 'traditional', + featuresPluginMock.createStart() ); const id = savedObject.id; const actualSpace = await client.get(id); @@ -234,7 +241,8 @@ describe('#get', () => { mockConfig, mockCallWithRequestRepository, [], - 'serverless' + 'serverless', + featuresPluginMock.createStart() ); const id = savedObject.id; const actualSpace = await client.get(id); @@ -257,7 +265,8 @@ describe('#get', () => { mockConfig, mockCallWithRequestRepository, [], - 'traditional' + 'traditional', + featuresPluginMock.createStart() ); const id = savedObject.id; const actualSpace = await client.get(id); @@ -320,7 +329,8 @@ describe('#create', () => { mockConfig, mockCallWithRequestRepository, [], - 'traditional' + 'traditional', + featuresPluginMock.createStart() ); const actualSpace = await client.create(spaceToCreate); @@ -336,6 +346,60 @@ describe('#create', () => { }); }); + test(`throws bad request when creating space with disabled features`, async () => { + const maxSpaces = 5; + const mockDebugLogger = createMockDebugLogger(); + const mockConfig = createMockConfig(); + const mockCallWithRequestRepository = savedObjectsRepositoryMock.create(); + mockCallWithRequestRepository.create.mockResolvedValue(savedObject); + mockCallWithRequestRepository.find.mockResolvedValue({ + total: maxSpaces - 1, + } as any); + const featuresMock = featuresPluginMock.createStart(); + + featuresMock.getKibanaFeatures.mockReturnValue([ + new KibanaFeature({ + id: 'feature-1', + name: 'KibanaFeature', + app: [], + category: { id: 'foo', label: 'foo' }, + scope: [KibanaFeatureScope.Security], + privileges: { + all: { + savedObject: { + all: [], + read: [], + }, + ui: ['foo'], + }, + read: { + savedObject: { + all: [], + read: [], + }, + ui: ['foo'], + }, + }, + subFeatures: [], + }), + ]); + + const client = new SpacesClient( + mockDebugLogger, + mockConfig, + mockCallWithRequestRepository, + [], + 'traditional', + featuresMock + ); + + await expect( + client.create({ ...spaceToCreate, disabledFeatures: ['feature-1'] }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unable to create Space, one or more disabledFeatures do not have the required space scope"` + ); + }); + test(`throws bad request when we are at the maximum number of spaces`, async () => { const maxSpaces = 5; const mockDebugLogger = createMockDebugLogger(); @@ -357,7 +421,8 @@ describe('#create', () => { mockConfig, mockCallWithRequestRepository, [], - 'traditional' + 'traditional', + featuresPluginMock.createStart() ); await expect(client.create(spaceToCreate)).rejects.toThrowErrorMatchingInlineSnapshot( @@ -393,7 +458,8 @@ describe('#create', () => { mockConfig, mockCallWithRequestRepository, [], - 'serverless' + 'serverless', + featuresPluginMock.createStart() ); await expect( @@ -440,7 +506,8 @@ describe('#create', () => { mockConfig, mockCallWithRequestRepository, [], - 'traditional' + 'traditional', + featuresPluginMock.createStart() ); const actualSpace = await client.create({ ...spaceToCreate, solution: 'es' }); @@ -483,7 +550,8 @@ describe('#create', () => { mockConfig, mockCallWithRequestRepository, [], - 'traditional' + 'traditional', + featuresPluginMock.createStart() ); const actualSpace = await client.create(spaceToCreate); @@ -520,7 +588,8 @@ describe('#create', () => { mockConfig, mockCallWithRequestRepository, [], - 'traditional' + 'traditional', + featuresPluginMock.createStart() ); await expect( @@ -560,7 +629,8 @@ describe('#create', () => { mockConfig, mockCallWithRequestRepository, [], - 'traditional' + 'traditional', + featuresPluginMock.createStart() ); await expect( @@ -624,7 +694,8 @@ describe('#update', () => { mockConfig, mockCallWithRequestRepository, [], - 'traditional' + 'traditional', + featuresPluginMock.createStart() ); const id = savedObject.id; const actualSpace = await client.update(id, spaceToUpdate); @@ -634,6 +705,56 @@ describe('#update', () => { expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); }); + test(`throws bad request when creating space with disabled features`, async () => { + const mockDebugLogger = createMockDebugLogger(); + const mockConfig = createMockConfig(); + const mockCallWithRequestRepository = savedObjectsRepositoryMock.create(); + mockCallWithRequestRepository.get.mockResolvedValue(savedObject); + const featuresMock = featuresPluginMock.createStart(); + + featuresMock.getKibanaFeatures.mockReturnValue([ + new KibanaFeature({ + id: 'feature-1', + name: 'KibanaFeature', + app: [], + category: { id: 'foo', label: 'foo' }, + scope: [KibanaFeatureScope.Security], + privileges: { + all: { + savedObject: { + all: [], + read: [], + }, + ui: ['foo'], + }, + read: { + savedObject: { + all: [], + read: [], + }, + ui: ['foo'], + }, + }, + subFeatures: [], + }), + ]); + + const client = new SpacesClient( + mockDebugLogger, + mockConfig, + mockCallWithRequestRepository, + [], + 'traditional', + featuresMock + ); + + await expect( + client.create({ ...spaceToUpdate, disabledFeatures: ['feature-1'] }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Cannot destructure property 'total' of '(intermediate value)' as it is undefined."` + ); + }); + test('throws bad request when solution property is provided in serverless build', async () => { const mockDebugLogger = createMockDebugLogger(); const mockConfig = createMockConfig(); @@ -645,7 +766,8 @@ describe('#update', () => { mockConfig, mockCallWithRequestRepository, [], - 'serverless' + 'serverless', + featuresPluginMock.createStart() ); const id = savedObject.id; @@ -677,7 +799,8 @@ describe('#update', () => { mockConfig, mockCallWithRequestRepository, [], - 'traditional' + 'traditional', + featuresPluginMock.createStart() ); const id = savedObject.id; @@ -703,7 +826,8 @@ describe('#update', () => { mockConfig, mockCallWithRequestRepository, [], - 'traditional' + 'traditional', + featuresPluginMock.createStart() ); const id = savedObject.id; await client.update(id, { ...spaceToUpdate, solution: 'es' }); @@ -732,7 +856,8 @@ describe('#update', () => { mockConfig, mockCallWithRequestRepository, [], - 'traditional' + 'traditional', + featuresPluginMock.createStart() ); const id = savedObject.id; const actualSpace = await client.update(id, spaceToUpdate); @@ -758,7 +883,8 @@ describe('#update', () => { mockConfig, mockCallWithRequestRepository, [], - 'traditional' + 'traditional', + featuresPluginMock.createStart() ); const id = savedObject.id; @@ -790,7 +916,8 @@ describe('#update', () => { mockConfig, mockCallWithRequestRepository, [], - 'traditional' + 'traditional', + featuresPluginMock.createStart() ); const id = savedObject.id; @@ -843,7 +970,8 @@ describe('#delete', () => { mockConfig, mockCallWithRequestRepository, [], - 'traditional' + 'traditional', + featuresPluginMock.createStart() ); await expect(client.delete(id)).rejects.toThrowErrorMatchingInlineSnapshot( @@ -864,7 +992,8 @@ describe('#delete', () => { mockConfig, mockCallWithRequestRepository, [], - 'traditional' + 'traditional', + featuresPluginMock.createStart() ); await client.delete(id); @@ -886,7 +1015,8 @@ describe('#disableLegacyUrlAliases', () => { mockConfig, mockCallWithRequestRepository, [], - 'traditional' + 'traditional', + featuresPluginMock.createStart() ); const aliases = [ { targetSpace: 'space1', targetType: 'foo', sourceId: '123' }, diff --git a/x-pack/plugins/spaces/server/spaces_client/spaces_client.ts b/x-pack/plugins/spaces/server/spaces_client/spaces_client.ts index e94918a62b26b..4043da9f87225 100644 --- a/x-pack/plugins/spaces/server/spaces_client/spaces_client.ts +++ b/x-pack/plugins/spaces/server/spaces_client/spaces_client.ts @@ -14,6 +14,8 @@ import type { SavedObject, } from '@kbn/core/server'; import type { LegacyUrlAliasTarget } from '@kbn/core-saved-objects-common'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; +import type { FeaturesPluginStart } from '@kbn/features-plugin/server'; import { isReservedSpace } from '../../common'; import type { spaceV1 as v1 } from '../../common'; @@ -88,7 +90,8 @@ export class SpacesClient implements ISpacesClient { private readonly config: ConfigType, private readonly repository: ISavedObjectsRepository, private readonly nonGlobalTypeNames: string[], - private readonly buildFlavour: BuildFlavor + private readonly buildFlavour: BuildFlavor, + private readonly features: FeaturesPluginStart ) { this.isServerless = this.buildFlavour === 'serverless'; } @@ -150,6 +153,8 @@ export class SpacesClient implements ISpacesClient { throw Boom.badRequest('Unable to create Space, solution property cannot be empty'); } + this.validateDisabledFeatures(space); + this.debugLogger(`SpacesClient.create(), using RBAC. Attempting to create space`); const id = space.id; @@ -183,6 +188,8 @@ export class SpacesClient implements ISpacesClient { throw Boom.badRequest('Unable to update Space, solution property cannot be empty'); } + this.validateDisabledFeatures(space); + const attributes = this.generateSpaceAttributes(space); await this.repository.update('space', id, attributes); const updatedSavedObject = await this.repository.get('space', id); @@ -216,6 +223,28 @@ export class SpacesClient implements ISpacesClient { await this.repository.bulkUpdate(objectsToUpdate); } + private validateDisabledFeatures = (space: v1.Space) => { + if (!space.disabledFeatures.length || this.isServerless) { + return; + } + + const kibanaFeatures = this.features.getKibanaFeatures(); + + if ( + space.disabledFeatures.some((feature) => { + const disabledKibanaFeature = kibanaFeatures.find((f) => f.id === feature); + + return ( + disabledKibanaFeature && !disabledKibanaFeature.scope?.includes(KibanaFeatureScope.Spaces) + ); + }) + ) { + throw Boom.badRequest( + 'Unable to create Space, one or more disabledFeatures do not have the required space scope' + ); + } + }; + private transformSavedObjectToSpace = (savedObject: SavedObject): v1.Space => { return { id: savedObject.id, diff --git a/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.test.ts b/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.test.ts index cedcdec858e55..6381148a35c4b 100644 --- a/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.test.ts +++ b/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.test.ts @@ -8,6 +8,7 @@ import * as Rx from 'rxjs'; import { coreMock, httpServerMock } from '@kbn/core/server/mocks'; +import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; import type { ISpacesClient } from './spaces_client'; import { SpacesClient } from './spaces_client'; @@ -49,7 +50,7 @@ describe('SpacesClientService', () => { const service = new SpacesClientService(debugLogger, 'traditional'); service.setup({ config$: new Rx.Observable() }); const coreStart = coreMock.createStart(); - const start = service.start(coreStart); + const start = service.start(coreStart, featuresPluginMock.createStart()); const request = httpServerMock.createKibanaRequest(); @@ -64,7 +65,7 @@ describe('SpacesClientService', () => { service.setup({ config$: Rx.of(spacesConfig) }); const coreStart = coreMock.createStart(); - const start = service.start(coreStart); + const start = service.start(coreStart, featuresPluginMock.createStart()); const request = httpServerMock.createKibanaRequest(); const client = start.createSpacesClient(request); @@ -85,7 +86,7 @@ describe('SpacesClientService', () => { setup.setClientRepositoryFactory(customRepositoryFactory); const coreStart = coreMock.createStart(); - const start = service.start(coreStart); + const start = service.start(coreStart, featuresPluginMock.createStart()); const request = httpServerMock.createKibanaRequest(); const client = start.createSpacesClient(request); @@ -107,7 +108,7 @@ describe('SpacesClientService', () => { setup.registerClientWrapper(clientWrapper); const coreStart = coreMock.createStart(); - const start = service.start(coreStart); + const start = service.start(coreStart, featuresPluginMock.createStart()); const request = httpServerMock.createKibanaRequest(); const client = start.createSpacesClient(request); @@ -135,7 +136,7 @@ describe('SpacesClientService', () => { setup.registerClientWrapper(clientWrapper); const coreStart = coreMock.createStart(); - const start = service.start(coreStart); + const start = service.start(coreStart, featuresPluginMock.createStart()); const request = httpServerMock.createKibanaRequest(); const client = start.createSpacesClient(request); diff --git a/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.ts b/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.ts index 26d4fef85ea6d..6f3c905e0ea28 100644 --- a/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.ts +++ b/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.ts @@ -14,6 +14,7 @@ import type { KibanaRequest, SavedObjectsServiceStart, } from '@kbn/core/server'; +import type { FeaturesPluginStart } from '@kbn/features-plugin/server'; import type { ISpacesClient } from './spaces_client'; import { SpacesClient } from './spaces_client'; @@ -99,7 +100,7 @@ export class SpacesClientService { }; } - public start(coreStart: CoreStart): SpacesClientServiceStart { + public start(coreStart: CoreStart, features: FeaturesPluginStart): SpacesClientServiceStart { const nonGlobalTypes = coreStart.savedObjects .getTypeRegistry() .getAllTypes() @@ -122,7 +123,8 @@ export class SpacesClientService { this.config, this.repositoryFactory!(request, coreStart.savedObjects), nonGlobalTypeNames, - this.buildFlavour + this.buildFlavour, + features ); if (this.clientWrapper) { return this.clientWrapper(request, baseClient); diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts index 8222b43018b2e..b6be690e0e9dd 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts @@ -10,13 +10,13 @@ import * as Rx from 'rxjs'; import type { HttpServiceSetup, KibanaRequest, SavedObjectsRepository } from '@kbn/core/server'; import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import { coreMock, httpServerMock } from '@kbn/core/server/mocks'; +import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; import { SpacesService } from './spaces_service'; import { DEFAULT_SPACE_ID } from '../../common/constants'; import { getSpaceIdFromPath } from '../../common/lib/spaces_url_parser'; import { spacesConfig } from '../lib/__fixtures__'; import { SpacesClientService } from '../spaces_client'; - const createService = (serverBasePath: string = '') => { const spacesService = new SpacesService(); @@ -74,7 +74,10 @@ const createService = (serverBasePath: string = '') => { config$: Rx.of(spacesConfig), }); - const spacesClientServiceStart = spacesClientService.start(coreStart); + const spacesClientServiceStart = spacesClientService.start( + coreStart, + featuresPluginMock.createStart() + ); const spacesServiceStart = spacesService.start({ basePath: coreStart.http.basePath, diff --git a/x-pack/plugins/stack_alerts/server/feature.ts b/x-pack/plugins/stack_alerts/server/feature.ts index 1a54134d2cdc1..8f3c809829c49 100644 --- a/x-pack/plugins/stack_alerts/server/feature.ts +++ b/x-pack/plugins/stack_alerts/server/feature.ts @@ -11,6 +11,7 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { TRANSFORM_RULE_TYPE } from '@kbn/transform-plugin/common'; import { STACK_ALERTS_FEATURE_ID } from '@kbn/rule-data-utils'; import { ES_QUERY_ID as ElasticsearchQuery } from '@kbn/rule-data-utils'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { ID as IndexThreshold } from './rule_types/index_threshold/rule_type'; import { GEO_CONTAINMENT_ID as GeoContainment } from './rule_types/geo_containment'; @@ -23,6 +24,7 @@ export const BUILT_IN_ALERTS_FEATURE: KibanaFeatureConfig = { }), app: [], category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], management: { insightsAndAlerting: ['triggersActions'], }, diff --git a/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/plugin.ts index 90490a2ce0216..2c3df15d0f395 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/plugin.ts @@ -16,6 +16,7 @@ import { PluginStartContract as ActionsPluginStartContract, } from '@kbn/actions-plugin/server/plugin'; import { ActionType } from '@kbn/actions-plugin/server'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { initPlugin as initPagerduty } from './pagerduty_simulation'; import { initPlugin as initSwimlane } from './swimlane_simulation'; import { initPlugin as initServiceNow } from './servicenow_simulation'; @@ -127,6 +128,7 @@ export class FixturePlugin implements Plugin { + const { body } = await supertest.get('/api/features').expect(200); + expect(body).to.be.an(Array); + + const scopeAgnosticFeatures = [ + 'discover', + 'visualize', + 'dashboard', + 'dev_tools', + 'actions', + 'enterpriseSearch', + 'filesManagement', + 'filesSharedImage', + 'advancedSettings', + 'aiAssistantManagementSelection', + 'indexPatterns', + 'graph', + 'guidedOnboardingFeature', + 'monitoring', + 'observabilityAIAssistant', + 'observabilityCases', + 'savedObjectsManagement', + 'savedQueryManagement', + 'savedObjectsTagging', + 'ml', + 'apm', + 'stackAlerts', + 'canvas', + 'generalCases', + 'infrastructure', + 'logs', + 'maintenanceWindow', + 'maps', + 'osquery', + 'rulesSettings', + 'uptime', + 'searchInferenceEndpoints', + 'siem', + 'slo', + 'securitySolutionAssistant', + 'securitySolutionAttackDiscovery', + 'securitySolutionCases', + 'fleet', + 'fleetv2', + ]; + + const features = body.filter( + (f: KibanaFeature) => + f.scope?.includes(KibanaFeatureScope.Spaces) && + f.scope?.includes(KibanaFeatureScope.Security) + ); + + expect(features.every((f: KibanaFeature) => scopeAgnosticFeatures.includes(f.id))).to.be( + true + ); + }); }); }); } diff --git a/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts b/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts index 399a9b3d3fdcc..a10bf8ed1797e 100644 --- a/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts +++ b/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts @@ -12,6 +12,7 @@ import { SecurityPluginStart } from '@kbn/security-plugin/server'; import type { CasesServerStart, CasesServerSetup } from '@kbn/cases-plugin/server'; import { FilesSetup } from '@kbn/files-plugin/server'; import { PluginStartContract as ActionsPluginsStart } from '@kbn/actions-plugin/server/plugin'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; import { getPersistableStateAttachment } from './attachments/persistable_state'; import { getExternalReferenceAttachment } from './attachments/external_reference'; @@ -54,6 +55,7 @@ export class FixturePlugin implements Plugin new FooPlugin(); @@ -20,6 +21,7 @@ class FooPlugin implements Plugin { id: 'foo', name: 'Foo', category: { id: 'foo', label: 'foo' }, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], app: ['foo_plugin', 'kibana'], catalogue: ['foo'], privileges: { From d4ffb9bc03105a7d68dc1925fef293ad22b6ef95 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 20 Sep 2024 01:43:56 +1000 Subject: [PATCH 13/19] [8.x] [Cloud Security] User Name Misconfiguration Table and Preview Contextual Flyout (#192946) (#193438) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [[Cloud Security] User Name Misconfiguration Table and Preview Contextual Flyout (#192946)](https://github.com/elastic/kibana/pull/192946) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Rickyanto Ang --- .../csp_details/insights_tab_csp.tsx | 41 +------------ ...isconfiguration_findings_details_table.tsx | 12 ++-- .../components/index.tsx | 18 +++++- .../misconfiguration_preview.test.tsx | 5 +- .../misconfiguration_preview.tsx | 61 ++++++++++++------- .../entity_details/host_right/content.tsx | 2 +- .../user_details_left/index.test.tsx | 42 +++++++++++++ .../user_details_left/index.tsx | 29 +++++++-- .../entity_details/user_details_left/tabs.tsx | 14 ++++- .../entity_details/user_right/content.tsx | 2 + .../entity_details/user_right/index.tsx | 32 +++++++++- .../security_solution/public/flyout/index.tsx | 4 +- 12 files changed, 175 insertions(+), 87 deletions(-) diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/insights_tab_csp.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/insights_tab_csp.tsx index fa91a99c858a8..595aaf5127ca3 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/insights_tab_csp.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/insights_tab_csp.tsx @@ -6,55 +6,16 @@ */ import React, { memo } from 'react'; -import { EuiButtonGroup, EuiSpacer } from '@elastic/eui'; -import type { EuiButtonGroupOptionProps } from '@elastic/eui/src/components/button/button_group/button_group'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { useExpandableFlyoutState } from '@kbn/expandable-flyout'; +import { EuiSpacer } from '@elastic/eui'; import { MisconfigurationFindingsDetailsTable } from './misconfiguration_findings_details_table'; -enum InsightsTabCspTab { - MISCONFIGURATION = 'misconfigurationTabId', -} - -const insightsButtons: EuiButtonGroupOptionProps[] = [ - { - id: InsightsTabCspTab.MISCONFIGURATION, - label: ( - - ), - 'data-test-subj': 'misconfigurationTabDataTestId', - }, -]; - /** * Insights view displayed in the document details expandable flyout left section */ export const InsightsTabCsp = memo( ({ name, fieldName }: { name: string; fieldName: 'host.name' | 'user.name' }) => { - const panels = useExpandableFlyoutState(); - const activeInsightsId = panels.left?.path?.subTab ?? 'misconfigurationTabId'; - return ( <> - {}} - buttonSize="compressed" - isFullWidth - data-test-subj={'insightButtonGroupsTestId'} - /> diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/misconfiguration_findings_details_table.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/misconfiguration_findings_details_table.tsx index 1362e0e42e6ba..ba413709d6cca 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/misconfiguration_findings_details_table.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/csp_details/misconfiguration_findings_details_table.tsx @@ -102,14 +102,14 @@ export const MisconfigurationFindingsDetailsTable = memo( const navToFindings = useNavigateFindings(); - const navToFindingsByHostName = (hostName: string) => { - navToFindings({ 'host.name': hostName }, ['rule.name']); - }; - const navToFindingsByRuleAndResourceId = (ruleId: string, resourceId: string) => { navToFindings({ 'rule.id': ruleId, 'resource.id': resourceId }); }; + const navToFindingsByName = (name: string, queryField: 'host.name' | 'user.name') => { + navToFindings({ [queryField]: name }, ['rule.name']); + }; + const columns: Array> = [ { field: 'rule', @@ -154,13 +154,13 @@ export const MisconfigurationFindingsDetailsTable = memo( { - navToFindingsByHostName(queryName); + navToFindingsByName(queryName, fieldName); }} > {i18n.translate( 'xpack.securitySolution.flyout.left.insights.misconfigurations.tableTitle', { - defaultMessage: 'Misconfigurations', + defaultMessage: 'Misconfigurations ', } )} diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/index.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/index.tsx index 3058300036565..6045a8b8c9a5e 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/index.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/index.tsx @@ -13,7 +13,15 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { useCspSetupStatusApi } from '@kbn/cloud-security-posture/src/hooks/use_csp_setup_status_api'; import { MisconfigurationsPreview } from './misconfiguration/misconfiguration_preview'; -export const EntityInsight = ({ hostName }: { hostName: string }) => { +export const EntityInsight = ({ + name, + fieldName, + isPreviewMode, +}: { + name: string; + fieldName: 'host.name' | 'user.name'; + isPreviewMode?: boolean; +}) => { const { euiTheme } = useEuiTheme(); const getSetupStatus = useCspSetupStatusApi(); const hasMisconfigurationFindings = getSetupStatus.data?.hasMisconfigurationsFindings; @@ -22,7 +30,6 @@ export const EntityInsight = ({ hostName }: { hostName: string }) => { <> {hasMisconfigurationFindings && ( <> - ({ hostName }: { hostName: string }) => { } > - + + )} diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.test.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.test.tsx index 1c4c2adb60218..2e10d481b9934 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.test.tsx @@ -11,8 +11,9 @@ import { render } from '@testing-library/react'; import React from 'react'; import { MisconfigurationsPreview } from './misconfiguration_preview'; -const mockProps = { - hostName: 'testContextID', +const mockProps: { name: string; fieldName: 'host.name' | 'user.name' } = { + name: 'testContextID', + fieldName: 'host.name', }; describe('MisconfigurationsPreview', () => { diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx index f6ba0389f752a..e6c3950e81583 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/misconfiguration/misconfiguration_preview.tsx @@ -17,10 +17,12 @@ import { i18n } from '@kbn/i18n'; import { ExpandablePanel } from '@kbn/security-solution-common'; import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { UserDetailsPanelKey } from '../../../flyout/entity_details/user_details_left'; import { HostDetailsPanelKey } from '../../../flyout/entity_details/host_details_left'; import { useRiskScore } from '../../../entity_analytics/api/hooks/use_risk_score'; import { RiskScoreEntity } from '../../../../common/entity_analytics/risk_engine'; -import { buildHostNamesFilter } from '../../../../common/search_strategy'; +import type { HostRiskScore, UserRiskScore } from '../../../../common/search_strategy'; +import { buildHostNamesFilter, buildUserNamesFilter } from '../../../../common/search_strategy'; const FIRST_RECORD_PAGINATION = { cursorStart: 0, @@ -120,46 +122,63 @@ const MisconfigurationPreviewScore = ({ ); }; -export const MisconfigurationsPreview = ({ hostName }: { hostName: string }) => { +export const MisconfigurationsPreview = ({ + name, + fieldName, + isPreviewMode, +}: { + name: string; + fieldName: 'host.name' | 'user.name'; + isPreviewMode?: boolean; +}) => { const { data } = useMisconfigurationPreview({ - query: buildEntityFlyoutPreviewQuery('host.name', hostName), + query: buildEntityFlyoutPreviewQuery(fieldName, name), sort: [], enabled: true, pageSize: 1, }); - + const isUsingHostName = fieldName === 'host.name'; const passedFindings = data?.count.passed || 0; const failedFindings = data?.count.failed || 0; const { euiTheme } = useEuiTheme(); const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0; - const hostNameFilterQuery = useMemo( - () => (hostName ? buildHostNamesFilter([hostName]) : undefined), - [hostName] + + const buildFilterQuery = useMemo( + () => (isUsingHostName ? buildHostNamesFilter([name]) : buildUserNamesFilter([name])), + [isUsingHostName, name] ); const riskScoreState = useRiskScore({ - riskEntity: RiskScoreEntity.host, - filterQuery: hostNameFilterQuery, + riskEntity: isUsingHostName ? RiskScoreEntity.host : RiskScoreEntity.user, + filterQuery: buildFilterQuery, onlyLatest: false, pagination: FIRST_RECORD_PAGINATION, }); const { data: hostRisk } = riskScoreState; - const hostRiskData = hostRisk && hostRisk.length > 0 ? hostRisk[0] : undefined; - const isRiskScoreExist = !!hostRiskData?.host.risk; + const riskData = hostRisk?.[0]; + const isRiskScoreExist = isUsingHostName + ? !!(riskData as HostRiskScore)?.host.risk + : !!(riskData as UserRiskScore)?.user.risk; const { openLeftPanel } = useExpandableFlyoutApi(); - const isPreviewMode = false; const goToEntityInsightTab = useCallback(() => { openLeftPanel({ - id: HostDetailsPanelKey, - params: { - name: hostName, - isRiskScoreExist, - hasMisconfigurationFindings, - path: { tab: 'csp_insights' }, - }, + id: isUsingHostName ? HostDetailsPanelKey : UserDetailsPanelKey, + params: isUsingHostName + ? { + name, + isRiskScoreExist, + hasMisconfigurationFindings, + path: { tab: 'csp_insights' }, + } + : { + user: { name }, + isRiskScoreExist, + hasMisconfigurationFindings, + path: { tab: 'csp_insights' }, + }, }); - }, [hasMisconfigurationFindings, hostName, isRiskScoreExist, openLeftPanel]); + }, [hasMisconfigurationFindings, isRiskScoreExist, isUsingHostName, name, openLeftPanel]); const link = useMemo( () => !isPreviewMode @@ -178,7 +197,7 @@ export const MisconfigurationsPreview = ({ hostName }: { hostName: string }) => return ( + - ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.test.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.test.tsx index bdff465e0b982..9c4b9938d6daa 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.test.tsx @@ -51,4 +51,46 @@ describe('LeftPanel', () => { expect(tabElement).not.toBeInTheDocument(); }); + + it("doesn't render insights panel when there no misconfiguration findings", () => { + const { queryByText } = render( + , + { + wrapper: TestProviders, + } + ); + + const tabElement = queryByText('Insights'); + + expect(tabElement).not.toBeInTheDocument(); + }); + + it('render insights panel when there are misconfiguration findings', () => { + const { queryByText } = render( + , + { + wrapper: TestProviders, + } + ); + + const tabElement = queryByText('Insights'); + + expect(tabElement).toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx index a04bd739eb299..ae3e99cc17cfe 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/index.tsx @@ -28,6 +28,7 @@ export interface UserDetailsPanelProps extends Record { user: UserParam; path?: PanelPath; scopeId: string; + hasMisconfigurationFindings?: boolean; } export interface UserDetailsExpandableFlyoutProps extends FlyoutPanelProps { key: 'user_details'; @@ -40,10 +41,24 @@ export const UserDetailsPanel = ({ user, path, scopeId, + hasMisconfigurationFindings, }: UserDetailsPanelProps) => { const managedUser = useManagedUser(user.name, user.email); - const tabs = useTabs(managedUser.data, user.name, isRiskScoreExist, scopeId); - const { selectedTabId, setSelectedTabId } = useSelectedTab(isRiskScoreExist, user, tabs, path); + const tabs = useTabs( + managedUser.data, + user.name, + isRiskScoreExist, + scopeId, + hasMisconfigurationFindings + ); + + const { selectedTabId, setSelectedTabId } = useSelectedTab( + isRiskScoreExist, + user, + tabs, + path, + hasMisconfigurationFindings + ); if (managedUser.isLoading) return ; @@ -67,7 +82,8 @@ const useSelectedTab = ( isRiskScoreExist: boolean, user: UserParam, tabs: LeftPanelTabsType, - path: PanelPath | undefined + path: PanelPath | undefined, + hasMisconfigurationFindings?: boolean ) => { const { openLeftPanel } = useExpandableFlyoutApi(); @@ -81,12 +97,13 @@ const useSelectedTab = ( const setSelectedTabId = (tabId: EntityDetailsLeftPanelTab) => { openLeftPanel({ id: UserDetailsPanelKey, - path: { - tab: tabId, - }, params: { user, isRiskScoreExist, + hasMisconfigurationFindings, + path: { + tab: tabId, + }, }, }); }; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx index 3a6814a28e62c..6f27b054759f2 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_details_left/tabs.tsx @@ -8,7 +8,10 @@ import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { getRiskInputTab } from '../../../entity_analytics/components/entity_details_flyout'; +import { + getInsightsInputTab, + getRiskInputTab, +} from '../../../entity_analytics/components/entity_details_flyout'; import { UserAssetTableType } from '../../../explore/users/store/model'; import { ManagedUserDatasetKey } from '../../../../common/search_strategy/security_solution/users/managed_details'; import type { @@ -26,7 +29,8 @@ export const useTabs = ( managedUser: ManagedUserHits, name: string, isRiskScoreExist: boolean, - scopeId: string + scopeId: string, + hasMisconfigurationFindings?: boolean ): LeftPanelTabsType => useMemo(() => { const tabs: LeftPanelTabsType = []; @@ -51,8 +55,12 @@ export const useTabs = ( tabs.push(getEntraTab(entraManagedUser)); } + if (hasMisconfigurationFindings) { + tabs.push(getInsightsInputTab({ name, fieldName: 'user.name' })); + } + return tabs; - }, [isRiskScoreExist, managedUser, name, scopeId]); + }, [hasMisconfigurationFindings, isRiskScoreExist, managedUser, name, scopeId]); const getOktaTab = (oktaManagedUser: ManagedUserHit) => ({ id: EntityDetailsLeftPanelTab.OKTA, diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx index 26945a12f8bd6..42b281d0c8d2b 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/content.tsx @@ -23,6 +23,7 @@ import { ObservedEntity } from '../shared/components/observed_entity'; import type { ObservedEntityData } from '../shared/components/observed_entity/types'; import { useObservedUserItems } from './hooks/use_observed_user_items'; import type { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header'; +import { EntityInsight } from '../../../cloud_security_posture/components'; interface UserPanelContentProps { userName: string; @@ -72,6 +73,7 @@ export const UserPanelContent = ({ entity={{ name: userName, type: 'user' }} onChange={onAssetCriticalityChange} /> + 0 || failedFindings > 0; + useQueryInspector({ deleteQuery, inspect, @@ -119,11 +134,20 @@ export const UserPanel = ({ name: userName, email, }, + path: tab ? { tab } : undefined, + hasMisconfigurationFindings, }, - path: tab ? { tab } : undefined, }); }, - [telemetry, openLeftPanel, userRiskData?.user?.risk, userName, email, scopeId] + [ + telemetry, + openLeftPanel, + userRiskData?.user?.risk, + scopeId, + userName, + email, + hasMisconfigurationFindings, + ] ); const openPanelFirstTab = useCallback(() => openPanelTab(), [openPanelTab]); @@ -156,7 +180,9 @@ export const UserPanel = ({ return ( <> ( - + ), }, { From b03acf34fec973ffb5773207314c16052d0ddb31 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 20 Sep 2024 01:46:04 +1000 Subject: [PATCH 14/19] [8.x] [Cloud Security] Fix GCP service account deployment copy-paste command (#192959) (#193427) # Backport This will backport the following commits from `main` to `8.x`: - [[Cloud Security] Fix GCP service account deployment copy-paste command (#192959)](https://github.com/elastic/kibana/pull/192959) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Or Ouziel --- .../gcp_credentials_form/gcp_credentials_form_agentless.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credentials_form/gcp_credentials_form_agentless.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credentials_form/gcp_credentials_form_agentless.tsx index 9cced3c87729b..4cec65cc695bb 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credentials_form/gcp_credentials_form_agentless.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credentials_form/gcp_credentials_form_agentless.tsx @@ -194,8 +194,8 @@ export const GcpCredentialsFormAgentless = ({ )?.replace(TEMPLATE_URL_ACCOUNT_TYPE_ENV_VAR, accountType); const commandText = `gcloud config set project ${ - isOrganization ? ` && ORD_ID=` : `` - } && ./deploy_service_account.sh`; + isOrganization ? ` && ORG_ID=` : `` + } ./deploy_service_account.sh`; return ( <> From 6ce928a0cd699886b6b33ab6f8aa9c5938154006 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 20 Sep 2024 01:47:23 +1000 Subject: [PATCH 15/19] [8.x] Add debug logging to session cleanup api integration test (#193259) (#193436) # Backport This will backport the following commits from `main` to `8.x`: - [Add debug logging to session cleanup api integration test (#193259)](https://github.com/elastic/kibana/pull/193259) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Sid --- .../tests/session_idle/cleanup.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts index 9af51a859befa..4a74e3938467e 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts @@ -18,6 +18,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); + const esSupertest = getService('esSupertest'); const es = getService('es'); const esDeleteAllIndices = getService('esDeleteAllIndices'); const config = getService('config'); @@ -93,9 +94,19 @@ export default function ({ getService }: FtrProviderContext) { }); } + async function addESDebugLoggingSettings() { + const addLogging = { + persistent: { + 'logger.org.elasticsearch.xpack.security.authc': 'debug', + }, + }; + await esSupertest.put('/_cluster/settings').send(addLogging).expect(200); + } + describe('Session Idle cleanup', () => { beforeEach(async () => { await es.cluster.health({ index: '.kibana_security_session*', wait_for_status: 'green' }); + await addESDebugLoggingSettings(); await esDeleteAllIndices('.kibana_security_session*'); }); From b0853caa12b6a792ff004080dca65632ffb69a12 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 20 Sep 2024 02:04:18 +1000 Subject: [PATCH 16/19] [8.x] [ES|QL] Fixes Incomplete string escaping or encoding error (#193384) (#193432) # Backport This will backport the following commits from `main` to `8.x`: - [[ES|QL] Fixes Incomplete string escaping or encoding error (#193384)](https://github.com/elastic/kibana/pull/193384) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Stratoula Kalafateli --- packages/kbn-esql-utils/src/utils/append_to_query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-esql-utils/src/utils/append_to_query.ts b/packages/kbn-esql-utils/src/utils/append_to_query.ts index 76f317d55aa5d..f4161be073a8d 100644 --- a/packages/kbn-esql-utils/src/utils/append_to_query.ts +++ b/packages/kbn-esql-utils/src/utils/append_to_query.ts @@ -36,7 +36,7 @@ export function appendWhereClauseToESQLQuery( default: operator = '=='; } - let filterValue = typeof value === 'string' ? `"${value.replace(/"/g, '\\"')}"` : value; + let filterValue = typeof value === 'string' ? `"${value.replace(/\"/g, '\\"')}"` : value; // Adding the backticks here are they are needed for special char fields let fieldName = `\`${field}\``; From b2e6263d9496d8e96b6c44447e01bc488f2d1d2c Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 20 Sep 2024 02:26:08 +1000 Subject: [PATCH 17/19] [8.x] [Embeddable Rebuild] [Controls] Clean up services + TODOs (#193180) (#193429) # Backport This will backport the following commits from `main` to `8.x`: - [[Embeddable Rebuild] [Controls] Clean up services + TODOs (#193180)](https://github.com/elastic/kibana/pull/193180) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Hannah Mudge --- src/plugins/controls/common/constants.ts | 4 +- .../controls/common/control_group/types.ts | 6 +- src/plugins/controls/common/index.ts | 4 +- .../controls/common/options_list/types.ts | 2 - src/plugins/controls/common/types.ts | 2 +- src/plugins/controls/jest_setup.ts | 7 +- src/plugins/controls/kibana.jsonc | 4 +- .../public/actions/clear_control_action.tsx | 49 ++------- ...lear_control_action_compatibility_check.ts | 42 ++++++++ .../actions/delete_control_action.test.tsx | 19 +--- .../public/actions/delete_control_action.tsx | 100 ++++++------------ ...lete_control_action_compatibility_check.ts | 42 ++++++++ .../actions/edit_control_action.test.tsx | 20 +--- src/plugins/controls/public/index.ts | 1 - src/plugins/controls/public/plugin.ts | 65 +++--------- .../components/control_error.tsx | 2 - .../components/control_group.tsx | 4 +- .../components/control_group_editor.test.tsx | 6 +- .../components/control_group_editor.tsx | 4 +- .../components/control_panel.test.tsx | 6 +- .../get_control_group_factory.tsx | 51 ++++----- .../open_edit_control_group_flyout.tsx | 15 ++- .../register_control_group_embeddable.ts | 17 +-- .../react_controls/control_group/types.ts | 6 +- .../utils/control_group_state_builder.ts | 7 +- .../utils/initialization_utils.ts | 4 +- .../data_control_editor.test.tsx | 9 +- .../data_controls/data_control_editor.tsx | 13 +-- .../data_control_editor_utils.ts | 2 - .../initialize_data_control.test.tsx | 21 ++-- .../data_controls/initialize_data_control.ts | 12 +-- .../open_data_control_editor.tsx | 18 ++-- .../fetch_and_validate.tsx | 5 +- .../get_options_list_control_factory.test.tsx | 16 +-- .../get_options_list_control_factory.tsx | 27 +++-- .../options_list_fetch_cache.ts | 11 +- .../register_options_list_control.ts | 17 ++- .../get_range_slider_control_factory.test.tsx | 16 +-- .../get_range_slider_control_factory.tsx | 27 +++-- .../range_slider/has_no_results.ts | 13 +-- .../data_controls/range_slider/min_max.ts | 13 +-- .../register_range_slider_control.ts | 17 ++- .../controls/data_controls/types.ts | 9 -- .../timeslider_control/get_time_range_meta.ts | 23 ++-- .../get_timeslider_control_factory.test.tsx | 17 ++- .../get_timeslider_control_factory.tsx | 11 +- .../init_time_range_subscription.ts | 11 +- .../register_timeslider_control.ts | 16 ++- .../controls/timeslider_control/types.ts | 7 -- .../public/react_controls/controls/types.ts | 11 +- .../control_group_renderer.test.tsx | 8 +- .../public/services/controls/controls.stub.ts | 46 -------- .../services/controls/controls_service.ts | 24 ----- .../public/services/controls/types.ts | 23 ---- .../public/services/core/core.stub.ts | 24 ----- .../public/services/core/core_service.ts | 28 ----- .../controls/public/services/core/types.ts | 17 --- .../public/services/data/data.stub.ts | 31 ------ .../public/services/data/data_service.ts | 29 ----- .../controls/public/services/data/types.ts | 15 --- .../services/data_views/data_views.stub.ts | 52 --------- .../services/data_views/data_views_service.ts | 29 ----- .../public/services/data_views/types.ts | 16 --- .../services/embeddable/embeddable.stub.ts | 19 ---- .../services/embeddable/embeddable_service.ts | 23 ---- .../public/services/embeddable/types.ts | 14 --- .../public/services/http/http.stub.ts | 19 ---- .../public/services/http/http_service.ts | 27 ----- .../controls/public/services/http/types.ts | 15 --- src/plugins/controls/public/services/index.ts | 10 -- .../public/services/kibana_services.ts | 42 ++++++++ src/plugins/controls/public/services/mocks.ts | 23 ++++ .../public/services/overlays/overlays.stub.ts | 33 ------ .../services/overlays/overlays_service.ts | 27 ----- .../public/services/overlays/types.ts | 20 ---- .../public/services/plugin_services.stub.ts | 54 ---------- .../public/services/plugin_services.ts | 52 --------- .../public/services/settings/settings.stub.ts | 18 ---- .../services/settings/settings_service.ts | 32 ------ .../public/services/settings/types.ts | 16 --- .../services/storage/storage_service.stub.ts | 20 ---- .../services/storage/storage_service.ts | 31 ------ .../controls/public/services/storage/types.ts | 13 --- src/plugins/controls/public/services/types.ts | 35 ------ .../public/services/unified_search/types.ts | 14 --- .../unified_search/unified_search.stub.ts | 27 ----- .../unified_search/unified_search_service.ts | 27 ----- src/plugins/controls/public/types.ts | 16 +-- .../control_group_persistence.ts | 4 +- .../options_list_cluster_settings_route.ts | 2 +- src/plugins/controls/tsconfig.json | 1 - 91 files changed, 399 insertions(+), 1408 deletions(-) create mode 100644 src/plugins/controls/public/actions/clear_control_action_compatibility_check.ts create mode 100644 src/plugins/controls/public/actions/delete_control_action_compatibility_check.ts delete mode 100644 src/plugins/controls/public/services/controls/controls.stub.ts delete mode 100644 src/plugins/controls/public/services/controls/controls_service.ts delete mode 100644 src/plugins/controls/public/services/controls/types.ts delete mode 100644 src/plugins/controls/public/services/core/core.stub.ts delete mode 100644 src/plugins/controls/public/services/core/core_service.ts delete mode 100644 src/plugins/controls/public/services/core/types.ts delete mode 100644 src/plugins/controls/public/services/data/data.stub.ts delete mode 100644 src/plugins/controls/public/services/data/data_service.ts delete mode 100644 src/plugins/controls/public/services/data/types.ts delete mode 100644 src/plugins/controls/public/services/data_views/data_views.stub.ts delete mode 100644 src/plugins/controls/public/services/data_views/data_views_service.ts delete mode 100644 src/plugins/controls/public/services/data_views/types.ts delete mode 100644 src/plugins/controls/public/services/embeddable/embeddable.stub.ts delete mode 100644 src/plugins/controls/public/services/embeddable/embeddable_service.ts delete mode 100644 src/plugins/controls/public/services/embeddable/types.ts delete mode 100644 src/plugins/controls/public/services/http/http.stub.ts delete mode 100644 src/plugins/controls/public/services/http/http_service.ts delete mode 100644 src/plugins/controls/public/services/http/types.ts delete mode 100644 src/plugins/controls/public/services/index.ts create mode 100644 src/plugins/controls/public/services/kibana_services.ts create mode 100644 src/plugins/controls/public/services/mocks.ts delete mode 100644 src/plugins/controls/public/services/overlays/overlays.stub.ts delete mode 100644 src/plugins/controls/public/services/overlays/overlays_service.ts delete mode 100644 src/plugins/controls/public/services/overlays/types.ts delete mode 100644 src/plugins/controls/public/services/plugin_services.stub.ts delete mode 100644 src/plugins/controls/public/services/plugin_services.ts delete mode 100644 src/plugins/controls/public/services/settings/settings.stub.ts delete mode 100644 src/plugins/controls/public/services/settings/settings_service.ts delete mode 100644 src/plugins/controls/public/services/settings/types.ts delete mode 100644 src/plugins/controls/public/services/storage/storage_service.stub.ts delete mode 100644 src/plugins/controls/public/services/storage/storage_service.ts delete mode 100644 src/plugins/controls/public/services/storage/types.ts delete mode 100644 src/plugins/controls/public/services/types.ts delete mode 100644 src/plugins/controls/public/services/unified_search/types.ts delete mode 100644 src/plugins/controls/public/services/unified_search/unified_search.stub.ts delete mode 100644 src/plugins/controls/public/services/unified_search/unified_search_service.ts diff --git a/src/plugins/controls/common/constants.ts b/src/plugins/controls/common/constants.ts index e100474177c71..e375a7b2315bc 100644 --- a/src/plugins/controls/common/constants.ts +++ b/src/plugins/controls/common/constants.ts @@ -7,11 +7,11 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ControlStyle, ControlWidth } from './types'; +import { ControlLabelPosition, ControlWidth } from './types'; export const DEFAULT_CONTROL_WIDTH: ControlWidth = 'medium'; export const DEFAULT_CONTROL_GROW: boolean = true; -export const DEFAULT_CONTROL_STYLE: ControlStyle = 'oneLine'; +export const DEFAULT_CONTROL_LABEL_POSITION: ControlLabelPosition = 'oneLine'; export const TIME_SLIDER_CONTROL = 'timeSlider'; export const RANGE_SLIDER_CONTROL = 'rangeSliderControl'; diff --git a/src/plugins/controls/common/control_group/types.ts b/src/plugins/controls/common/control_group/types.ts index cb51cf79e5400..eb47d8b13eb79 100644 --- a/src/plugins/controls/common/control_group/types.ts +++ b/src/plugins/controls/common/control_group/types.ts @@ -8,7 +8,7 @@ */ import { DataViewField } from '@kbn/data-views-plugin/common'; -import { ControlStyle, DefaultControlState, ParentIgnoreSettings } from '../types'; +import { ControlLabelPosition, DefaultControlState, ParentIgnoreSettings } from '../types'; export const CONTROL_GROUP_TYPE = 'control_group'; @@ -31,7 +31,7 @@ export interface ControlGroupEditorConfig { export interface ControlGroupRuntimeState { chainingSystem: ControlGroupChainingSystem; - labelPosition: ControlStyle; // TODO: Rename this type to ControlLabelPosition + labelPosition: ControlLabelPosition; autoApplySelections: boolean; ignoreParentSettings?: ParentIgnoreSettings; @@ -50,7 +50,7 @@ export interface ControlGroupSerializedState ignoreParentSettingsJSON: string; // In runtime state, we refer to this property as `labelPosition`; // to avoid migrations, we will continue to refer to this property as `controlStyle` in the serialized state - controlStyle: ControlStyle; + controlStyle: ControlLabelPosition; // In runtime state, we refer to the inverse of this property as `autoApplySelections` // to avoid migrations, we will continue to refer to this property as `showApplySelections` in the serialized state showApplySelections?: boolean; diff --git a/src/plugins/controls/common/index.ts b/src/plugins/controls/common/index.ts index c59e4c04ac1b0..dd9c56778bb68 100644 --- a/src/plugins/controls/common/index.ts +++ b/src/plugins/controls/common/index.ts @@ -8,7 +8,7 @@ */ export type { - ControlStyle, + ControlLabelPosition, ControlWidth, DefaultControlState, DefaultDataControlState, @@ -18,7 +18,7 @@ export type { export { DEFAULT_CONTROL_GROW, - DEFAULT_CONTROL_STYLE, + DEFAULT_CONTROL_LABEL_POSITION, DEFAULT_CONTROL_WIDTH, OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL, diff --git a/src/plugins/controls/common/options_list/types.ts b/src/plugins/controls/common/options_list/types.ts index e5eccbcab5cf5..10d4a88553586 100644 --- a/src/plugins/controls/common/options_list/types.ts +++ b/src/plugins/controls/common/options_list/types.ts @@ -15,8 +15,6 @@ import { OptionsListSortingType } from './suggestions_sorting'; import { DefaultDataControlState } from '../types'; import { OptionsListSearchTechnique } from './suggestions_searching'; -export const OPTIONS_LIST_CONTROL = 'optionsListControl'; // TODO: Replace with OPTIONS_LIST_CONTROL_TYPE - /** * ---------------------------------------------------------------- * Options list state types diff --git a/src/plugins/controls/common/types.ts b/src/plugins/controls/common/types.ts index 34d4708b3e991..d3a6261aeb9da 100644 --- a/src/plugins/controls/common/types.ts +++ b/src/plugins/controls/common/types.ts @@ -8,7 +8,7 @@ */ export type ControlWidth = 'small' | 'medium' | 'large'; -export type ControlStyle = 'twoLine' | 'oneLine'; +export type ControlLabelPosition = 'twoLine' | 'oneLine'; export type TimeSlice = [number, number]; diff --git a/src/plugins/controls/jest_setup.ts b/src/plugins/controls/jest_setup.ts index 722f87562328e..04a52e3b6653f 100644 --- a/src/plugins/controls/jest_setup.ts +++ b/src/plugins/controls/jest_setup.ts @@ -8,8 +8,5 @@ */ // Start the services with stubs -import { pluginServices } from './public/services'; -import { registry } from './public/services/plugin_services.stub'; - -registry.start({}); -pluginServices.setRegistry(registry); +import { setStubKibanaServices } from './public/services/mocks'; +setStubKibanaServices(); diff --git a/src/plugins/controls/kibana.jsonc b/src/plugins/controls/kibana.jsonc index bd65ecc2d0b6f..add8c14ee3391 100644 --- a/src/plugins/controls/kibana.jsonc +++ b/src/plugins/controls/kibana.jsonc @@ -9,8 +9,6 @@ "browser": true, "requiredPlugins": [ "presentationUtil", - "kibanaReact", - "expressions", "embeddable", "dataViews", "data", @@ -18,6 +16,6 @@ "uiActions" ], "extraPublicDirs": ["common"], - "requiredBundles": ["kibanaUtils"] + "requiredBundles": [] } } diff --git a/src/plugins/controls/public/actions/clear_control_action.tsx b/src/plugins/controls/public/actions/clear_control_action.tsx index b7c2777473fd5..02347ace2fd8d 100644 --- a/src/plugins/controls/public/actions/clear_control_action.tsx +++ b/src/plugins/controls/public/actions/clear_control_action.tsx @@ -11,42 +11,10 @@ import React, { SyntheticEvent } from 'react'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { - apiIsPresentationContainer, - type PresentationContainer, -} from '@kbn/presentation-containers'; -import { - apiCanAccessViewMode, - apiHasParentApi, - apiHasType, - apiHasUniqueId, - apiIsOfType, - type EmbeddableApiContext, - type HasParentApi, - type HasType, - type HasUniqueId, -} from '@kbn/presentation-publishing'; -import { type Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; +import type { EmbeddableApiContext, HasUniqueId } from '@kbn/presentation-publishing'; +import { IncompatibleActionError, type Action } from '@kbn/ui-actions-plugin/public'; import { ACTION_CLEAR_CONTROL } from '.'; -import { CONTROL_GROUP_TYPE } from '..'; -import { isClearableControl, type CanClearSelections } from '../types'; - -export type ClearControlActionApi = HasType & - HasUniqueId & - CanClearSelections & - HasParentApi; - -const isApiCompatible = (api: unknown | null): api is ClearControlActionApi => - Boolean( - apiHasType(api) && - apiHasUniqueId(api) && - isClearableControl(api) && - apiHasParentApi(api) && - apiCanAccessViewMode(api.parentApi) && - apiIsOfType(api.parentApi, CONTROL_GROUP_TYPE) && - apiIsPresentationContainer(api.parentApi) - ); export class ClearControlAction implements Action { public readonly type = ACTION_CLEAR_CONTROL; @@ -56,12 +24,10 @@ export class ClearControlAction implements Action { constructor() {} public readonly MenuItem = ({ context }: { context: EmbeddableApiContext }) => { - if (!isApiCompatible(context.embeddable)) throw new IncompatibleActionError(); - return ( ) => { @@ -75,23 +41,24 @@ export class ClearControlAction implements Action { }; public getDisplayName({ embeddable }: EmbeddableApiContext) { - if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); return i18n.translate('controls.controlGroup.floatingActions.clearTitle', { defaultMessage: 'Clear', }); } public getIconType({ embeddable }: EmbeddableApiContext) { - if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); return 'eraser'; } public async isCompatible({ embeddable }: EmbeddableApiContext) { - return isApiCompatible(embeddable); + const { isCompatible } = await import('./clear_control_action_compatibility_check'); + return isCompatible(embeddable); } public async execute({ embeddable }: EmbeddableApiContext) { - if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); + const { compatibilityCheck } = await import('./clear_control_action_compatibility_check'); + if (!compatibilityCheck(embeddable)) throw new IncompatibleActionError(); + embeddable.clearSelections(); } } diff --git a/src/plugins/controls/public/actions/clear_control_action_compatibility_check.ts b/src/plugins/controls/public/actions/clear_control_action_compatibility_check.ts new file mode 100644 index 0000000000000..f04cb91bc9a3a --- /dev/null +++ b/src/plugins/controls/public/actions/clear_control_action_compatibility_check.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { PresentationContainer, apiIsPresentationContainer } from '@kbn/presentation-containers'; +import { + HasParentApi, + HasType, + HasUniqueId, + apiCanAccessViewMode, + apiHasParentApi, + apiHasType, + apiHasUniqueId, + apiIsOfType, +} from '@kbn/presentation-publishing'; +import { CONTROL_GROUP_TYPE } from '../../common'; +import { isClearableControl, type CanClearSelections } from '../types'; + +type ClearControlActionApi = HasType & + HasUniqueId & + CanClearSelections & + HasParentApi; + +export const compatibilityCheck = (api: unknown | null): api is ClearControlActionApi => + Boolean( + apiHasType(api) && + apiHasUniqueId(api) && + isClearableControl(api) && + apiHasParentApi(api) && + apiCanAccessViewMode(api.parentApi) && + apiIsOfType(api.parentApi, CONTROL_GROUP_TYPE) && + apiIsPresentationContainer(api.parentApi) + ); + +export function isCompatible(api: unknown) { + return compatibilityCheck(api); +} diff --git a/src/plugins/controls/public/actions/delete_control_action.test.tsx b/src/plugins/controls/public/actions/delete_control_action.test.tsx index 65be8a65ecd6f..c158d743f69ae 100644 --- a/src/plugins/controls/public/actions/delete_control_action.test.tsx +++ b/src/plugins/controls/public/actions/delete_control_action.test.tsx @@ -9,22 +9,15 @@ import { BehaviorSubject } from 'rxjs'; -import { coreMock } from '@kbn/core/public/mocks'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { ViewMode } from '@kbn/presentation-publishing'; - import { getOptionsListControlFactory } from '../react_controls/controls/data_controls/options_list_control/get_options_list_control_factory'; import { OptionsListControlApi } from '../react_controls/controls/data_controls/options_list_control/types'; import { getMockedBuildApi, getMockedControlGroupApi, } from '../react_controls/controls/mocks/control_mocks'; -import { pluginServices } from '../services'; import { DeleteControlAction } from './delete_control_action'; - -const mockDataViews = dataViewPluginMocks.createStartContract(); -const mockCore = coreMock.createStart(); +import { coreServices } from '../services/kibana_services'; const dashboardApi = { viewMode: new BehaviorSubject('view'), @@ -38,11 +31,7 @@ const controlGroupApi = getMockedControlGroupApi(dashboardApi, { let controlApi: OptionsListControlApi; beforeAll(async () => { - const controlFactory = getOptionsListControlFactory({ - core: mockCore, - data: dataPluginMock.createStartContract(), - dataViews: mockDataViews, - }); + const controlFactory = getOptionsListControlFactory(); const uuid = 'testControl'; const control = await controlFactory.buildControl( @@ -72,7 +61,7 @@ test('Execute throws an error when called with an embeddable not in a parent', a describe('Execute should open a confirm modal', () => { test('Canceling modal will keep control', async () => { const spyOn = jest.fn().mockResolvedValue(false); - pluginServices.getServices().overlays.openConfirm = spyOn; + coreServices.overlays.openConfirm = spyOn; const deleteControlAction = new DeleteControlAction(); await deleteControlAction.execute({ embeddable: controlApi }); @@ -83,7 +72,7 @@ describe('Execute should open a confirm modal', () => { test('Confirming modal will delete control', async () => { const spyOn = jest.fn().mockResolvedValue(true); - pluginServices.getServices().overlays.openConfirm = spyOn; + coreServices.overlays.openConfirm = spyOn; const deleteControlAction = new DeleteControlAction(); await deleteControlAction.execute({ embeddable: controlApi }); diff --git a/src/plugins/controls/public/actions/delete_control_action.tsx b/src/plugins/controls/public/actions/delete_control_action.tsx index 45a7a20385627..7ee55ddd3da69 100644 --- a/src/plugins/controls/public/actions/delete_control_action.tsx +++ b/src/plugins/controls/public/actions/delete_control_action.tsx @@ -10,65 +10,25 @@ import React from 'react'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { ViewMode } from '@kbn/embeddable-plugin/public'; import { i18n } from '@kbn/i18n'; -import { - apiIsPresentationContainer, - type PresentationContainer, -} from '@kbn/presentation-containers'; -import { - apiCanAccessViewMode, - apiHasParentApi, - apiHasType, - apiHasUniqueId, - apiIsOfType, - getInheritedViewMode, - type EmbeddableApiContext, - type HasParentApi, - type HasType, - type HasUniqueId, - type PublishesViewMode, -} from '@kbn/presentation-publishing'; +import type { HasUniqueId, EmbeddableApiContext } from '@kbn/presentation-publishing'; import { IncompatibleActionError, type Action } from '@kbn/ui-actions-plugin/public'; import { ACTION_DELETE_CONTROL } from '.'; -import { CONTROL_GROUP_TYPE } from '..'; -import { pluginServices } from '../services'; - -export type DeleteControlActionApi = HasType & - HasUniqueId & - HasParentApi; - -const isApiCompatible = (api: unknown | null): api is DeleteControlActionApi => - Boolean( - apiHasType(api) && - apiHasUniqueId(api) && - apiHasParentApi(api) && - apiCanAccessViewMode(api.parentApi) && - apiIsOfType(api.parentApi, CONTROL_GROUP_TYPE) && - apiIsPresentationContainer(api.parentApi) - ); +import { coreServices } from '../services/kibana_services'; export class DeleteControlAction implements Action { public readonly type = ACTION_DELETE_CONTROL; public readonly id = ACTION_DELETE_CONTROL; public order = 100; // should always be last - private openConfirm; - - constructor() { - ({ - overlays: { openConfirm: this.openConfirm }, - } = pluginServices.getServices()); - } + constructor() {} public readonly MenuItem = ({ context }: { context: EmbeddableApiContext }) => { - if (!isApiCompatible(context.embeddable)) throw new IncompatibleActionError(); - return ( this.execute(context)} @@ -79,46 +39,46 @@ export class DeleteControlAction implements Action { }; public getDisplayName({ embeddable }: EmbeddableApiContext) { - if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); return i18n.translate('controls.controlGroup.floatingActions.removeTitle', { defaultMessage: 'Delete', }); } public getIconType({ embeddable }: EmbeddableApiContext) { - if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); return 'trash'; } public async isCompatible({ embeddable }: EmbeddableApiContext) { - return ( - isApiCompatible(embeddable) && getInheritedViewMode(embeddable.parentApi) === ViewMode.EDIT - ); + const { isCompatible } = await import('./delete_control_action_compatibility_check'); + return isCompatible(embeddable); } public async execute({ embeddable }: EmbeddableApiContext) { - if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); + const { compatibilityCheck } = await import('./delete_control_action_compatibility_check'); + if (!compatibilityCheck(embeddable)) throw new IncompatibleActionError(); - this.openConfirm( - i18n.translate('controls.controlGroup.management.delete.sub', { - defaultMessage: 'Controls are not recoverable once removed.', - }), - { - confirmButtonText: i18n.translate('controls.controlGroup.management.delete.confirm', { - defaultMessage: 'Delete', - }), - cancelButtonText: i18n.translate('controls.controlGroup.management.delete.cancel', { - defaultMessage: 'Cancel', + coreServices.overlays + .openConfirm( + i18n.translate('controls.controlGroup.management.delete.sub', { + defaultMessage: 'Controls are not recoverable once removed.', }), - title: i18n.translate('controls.controlGroup.management.delete.deleteTitle', { - defaultMessage: 'Delete control?', - }), - buttonColor: 'danger', - } - ).then((confirmed) => { - if (confirmed) { - embeddable.parentApi.removePanel(embeddable.uuid); - } - }); + { + confirmButtonText: i18n.translate('controls.controlGroup.management.delete.confirm', { + defaultMessage: 'Delete', + }), + cancelButtonText: i18n.translate('controls.controlGroup.management.delete.cancel', { + defaultMessage: 'Cancel', + }), + title: i18n.translate('controls.controlGroup.management.delete.deleteTitle', { + defaultMessage: 'Delete control?', + }), + buttonColor: 'danger', + } + ) + .then((confirmed) => { + if (confirmed) { + embeddable.parentApi.removePanel(embeddable.uuid); + } + }); } } diff --git a/src/plugins/controls/public/actions/delete_control_action_compatibility_check.ts b/src/plugins/controls/public/actions/delete_control_action_compatibility_check.ts new file mode 100644 index 0000000000000..a09b3448b2fc1 --- /dev/null +++ b/src/plugins/controls/public/actions/delete_control_action_compatibility_check.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ViewMode } from '@kbn/embeddable-plugin/public'; +import { PresentationContainer, apiIsPresentationContainer } from '@kbn/presentation-containers'; +import { + HasParentApi, + HasType, + HasUniqueId, + PublishesViewMode, + apiCanAccessViewMode, + apiHasParentApi, + apiHasType, + apiHasUniqueId, + apiIsOfType, + getInheritedViewMode, +} from '@kbn/presentation-publishing'; +import { CONTROL_GROUP_TYPE } from '../../common'; + +type DeleteControlActionApi = HasType & + HasUniqueId & + HasParentApi; + +export const compatibilityCheck = (api: unknown | null): api is DeleteControlActionApi => + Boolean( + apiHasType(api) && + apiHasUniqueId(api) && + apiHasParentApi(api) && + apiCanAccessViewMode(api.parentApi) && + apiIsOfType(api.parentApi, CONTROL_GROUP_TYPE) && + apiIsPresentationContainer(api.parentApi) + ); + +export function isCompatible(api: unknown) { + return compatibilityCheck(api) && getInheritedViewMode(api.parentApi) === ViewMode.EDIT; +} diff --git a/src/plugins/controls/public/actions/edit_control_action.test.tsx b/src/plugins/controls/public/actions/edit_control_action.test.tsx index 3c28feb907421..b1c24d779aaf6 100644 --- a/src/plugins/controls/public/actions/edit_control_action.test.tsx +++ b/src/plugins/controls/public/actions/edit_control_action.test.tsx @@ -9,9 +9,6 @@ import { BehaviorSubject } from 'rxjs'; -import { coreMock } from '@kbn/core/public/mocks'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import dateMath from '@kbn/datemath'; import type { TimeRange } from '@kbn/es-query'; import type { ViewMode } from '@kbn/presentation-publishing'; @@ -23,12 +20,10 @@ import { getMockedControlGroupApi, } from '../react_controls/controls/mocks/control_mocks'; import { getTimesliderControlFactory } from '../react_controls/controls/timeslider_control/get_timeslider_control_factory'; +import { dataService } from '../services/kibana_services'; import { EditControlAction } from './edit_control_action'; -const mockDataViews = dataViewPluginMocks.createStartContract(); -const mockCore = coreMock.createStart(); -const dataStartServiceMock = dataPluginMock.createStartContract(); -dataStartServiceMock.query.timefilter.timefilter.calculateBounds = (timeRange: TimeRange) => { +dataService.query.timefilter.timefilter.calculateBounds = (timeRange: TimeRange) => { const now = new Date(); return { min: dateMath.parse(timeRange.from, { forceNow: now }), @@ -48,11 +43,7 @@ const controlGroupApi = getMockedControlGroupApi(dashboardApi, { let optionsListApi: OptionsListControlApi; beforeAll(async () => { - const controlFactory = getOptionsListControlFactory({ - core: mockCore, - data: dataStartServiceMock, - dataViews: mockDataViews, - }); + const controlFactory = getOptionsListControlFactory(); const optionsListUuid = 'optionsListControl'; const optionsListControl = await controlFactory.buildControl( @@ -73,10 +64,7 @@ beforeAll(async () => { describe('Incompatible embeddables', () => { test('Action is incompatible with embeddables that are not editable', async () => { - const timeSliderFactory = getTimesliderControlFactory({ - core: mockCore, - data: dataStartServiceMock, - }); + const timeSliderFactory = getTimesliderControlFactory(); const timeSliderUuid = 'timeSliderControl'; const timeSliderControl = await timeSliderFactory.buildControl( {}, diff --git a/src/plugins/controls/public/index.ts b/src/plugins/controls/public/index.ts index 6a490248b8929..6c7a548cb091d 100644 --- a/src/plugins/controls/public/index.ts +++ b/src/plugins/controls/public/index.ts @@ -21,7 +21,6 @@ export { ACTION_CLEAR_CONTROL, ACTION_DELETE_CONTROL, ACTION_EDIT_CONTROL } from export type { DataControlApi, DataControlFactory, - DataControlServices, } from './react_controls/controls/data_controls/types'; export { diff --git a/src/plugins/controls/public/plugin.ts b/src/plugins/controls/public/plugin.ts index 039b960bfc3c5..c6e1a2873b169 100644 --- a/src/plugins/controls/public/plugin.ts +++ b/src/plugins/controls/public/plugin.ts @@ -10,63 +10,36 @@ import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; import { PANEL_HOVER_TRIGGER } from '@kbn/embeddable-plugin/public'; +import { ClearControlAction } from './actions/clear_control_action'; +import { DeleteControlAction } from './actions/delete_control_action'; +import { EditControlAction } from './actions/edit_control_action'; import { registerControlGroupEmbeddable } from './react_controls/control_group/register_control_group_embeddable'; import { registerOptionsListControl } from './react_controls/controls/data_controls/options_list_control/register_options_list_control'; import { registerRangeSliderControl } from './react_controls/controls/data_controls/range_slider/register_range_slider_control'; import { registerTimeSliderControl } from './react_controls/controls/timeslider_control/register_timeslider_control'; -import { controlsService } from './services/controls/controls_service'; -import type { - ControlsPluginSetup, - ControlsPluginSetupDeps, - ControlsPluginStart, - ControlsPluginStartDeps, -} from './types'; +import { setKibanaServices, untilPluginStartServicesReady } from './services/kibana_services'; +import type { ControlsPluginSetupDeps, ControlsPluginStartDeps } from './types'; export class ControlsPlugin - implements - Plugin< - ControlsPluginSetup, - ControlsPluginStart, - ControlsPluginSetupDeps, - ControlsPluginStartDeps - > + implements Plugin { - private async startControlsKibanaServices( - coreStart: CoreStart, - startPlugins: ControlsPluginStartDeps - ) { - const { registry, pluginServices } = await import('./services/plugin_services'); - pluginServices.setRegistry(registry.start({ coreStart, startPlugins })); - } - public setup( - _coreSetup: CoreSetup, + _coreSetup: CoreSetup, _setupPlugins: ControlsPluginSetupDeps - ): ControlsPluginSetup { - const { registerControlFactory } = controlsService; + ) { const { embeddable } = _setupPlugins; - registerControlGroupEmbeddable(_coreSetup, embeddable); - registerOptionsListControl(_coreSetup); - registerRangeSliderControl(_coreSetup); - registerTimeSliderControl(_coreSetup); - - return { - registerControlFactory, - }; + registerControlGroupEmbeddable(embeddable); + registerOptionsListControl(); + registerRangeSliderControl(); + registerTimeSliderControl(); } - public start(coreStart: CoreStart, startPlugins: ControlsPluginStartDeps): ControlsPluginStart { - this.startControlsKibanaServices(coreStart, startPlugins).then(async () => { - const { uiActions } = startPlugins; - - const [{ DeleteControlAction }, { EditControlAction }, { ClearControlAction }] = - await Promise.all([ - import('./actions/delete_control_action'), - import('./actions/edit_control_action'), - import('./actions/clear_control_action'), - ]); + public start(coreStart: CoreStart, startPlugins: ControlsPluginStartDeps) { + const { uiActions } = startPlugins; + setKibanaServices(coreStart, startPlugins); + untilPluginStartServicesReady().then(() => { const deleteControlAction = new DeleteControlAction(); uiActions.registerAction(deleteControlAction); uiActions.attachAction(PANEL_HOVER_TRIGGER, deleteControlAction.id); @@ -79,12 +52,6 @@ export class ControlsPlugin uiActions.registerAction(clearControlAction); uiActions.attachAction(PANEL_HOVER_TRIGGER, clearControlAction.id); }); - - const { getControlFactory, getAllControlTypes } = controlsService; - return { - getControlFactory, - getAllControlTypes, - }; } public stop() {} diff --git a/src/plugins/controls/public/react_controls/control_group/components/control_error.tsx b/src/plugins/controls/public/react_controls/control_group/components/control_error.tsx index 52b9e7d9b806a..2ef6b06faeedd 100644 --- a/src/plugins/controls/public/react_controls/control_group/components/control_error.tsx +++ b/src/plugins/controls/public/react_controls/control_group/components/control_error.tsx @@ -13,8 +13,6 @@ import { EuiButtonEmpty, EuiPopover } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { Markdown } from '@kbn/shared-ux-markdown'; -/** TODO: This file is duplicated from the controls plugin to avoid exporting it */ - interface ControlErrorProps { error: Error | string; } diff --git a/src/plugins/controls/public/react_controls/control_group/components/control_group.tsx b/src/plugins/controls/public/react_controls/control_group/components/control_group.tsx index 9c962113d1a7f..54e778684806a 100644 --- a/src/plugins/controls/public/react_controls/control_group/components/control_group.tsx +++ b/src/plugins/controls/public/react_controls/control_group/components/control_group.tsx @@ -30,7 +30,7 @@ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiToolTip } from ' import { css } from '@emotion/react'; import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; -import type { ControlStyle } from '../../../../common'; +import type { ControlLabelPosition } from '../../../../common'; import type { DefaultControlApi } from '../../controls/types'; import { ControlGroupStrings } from '../control_group_strings'; import { ControlsInOrder } from '../init_controls_manager'; @@ -49,7 +49,7 @@ interface Props { setControlApi: (uuid: string, controlApi: DefaultControlApi) => void; }; hasUnappliedSelections: boolean; - labelPosition: ControlStyle; + labelPosition: ControlLabelPosition; } export function ControlGroup({ diff --git a/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.test.tsx b/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.test.tsx index 79d0312b29537..b3705106afe2c 100644 --- a/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.test.tsx +++ b/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.test.tsx @@ -15,8 +15,8 @@ import { render } from '@testing-library/react'; import { ControlGroupApi } from '../../..'; import { ControlGroupChainingSystem, - ControlStyle, - DEFAULT_CONTROL_STYLE, + ControlLabelPosition, + DEFAULT_CONTROL_LABEL_POSITION, ParentIgnoreSettings, } from '../../../../common'; import { DefaultControlApi } from '../../controls/types'; @@ -33,7 +33,7 @@ describe('render', () => { onDeleteAll: () => {}, stateManager: { chainingSystem: new BehaviorSubject('HIERARCHICAL'), - labelPosition: new BehaviorSubject(DEFAULT_CONTROL_STYLE), + labelPosition: new BehaviorSubject(DEFAULT_CONTROL_LABEL_POSITION), autoApplySelections: new BehaviorSubject(true), ignoreParentSettings: new BehaviorSubject(undefined), }, diff --git a/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.tsx b/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.tsx index f908a557366fa..c4e7dc61476ba 100644 --- a/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.tsx +++ b/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.tsx @@ -27,7 +27,7 @@ import { } from '@elastic/eui'; import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; -import type { ControlStyle, ParentIgnoreSettings } from '../../../../common'; +import type { ControlLabelPosition, ParentIgnoreSettings } from '../../../../common'; import { CONTROL_LAYOUT_OPTIONS } from '../../controls/data_controls/editor_constants'; import type { ControlStateManager } from '../../controls/types'; import { ControlGroupStrings } from '../control_group_strings'; @@ -86,7 +86,7 @@ export const ControlGroupEditor = ({ onCancel, onSave, onDeleteAll, stateManager idSelected={selectedLabelPosition} legend={ControlGroupStrings.management.labelPosition.getLabelPositionLegend()} onChange={(newPosition: string) => { - stateManager.labelPosition.next(newPosition as ControlStyle); + stateManager.labelPosition.next(newPosition as ControlLabelPosition); }} /> diff --git a/src/plugins/controls/public/react_controls/control_group/components/control_panel.test.tsx b/src/plugins/controls/public/react_controls/control_group/components/control_panel.test.tsx index bbf8e8127813e..365c896bb908e 100644 --- a/src/plugins/controls/public/react_controls/control_group/components/control_panel.test.tsx +++ b/src/plugins/controls/public/react_controls/control_group/components/control_panel.test.tsx @@ -14,7 +14,7 @@ import { pluginServices as presentationUtilPluginServices } from '@kbn/presentat import { registry as presentationUtilServicesRegistry } from '@kbn/presentation-util-plugin/public/services/plugin_services.story'; import { render, waitFor } from '@testing-library/react'; -import type { ControlStyle, ControlWidth } from '../../../../common'; +import type { ControlLabelPosition, ControlWidth } from '../../../../common'; import { ControlPanel } from './control_panel'; describe('render', () => { @@ -74,7 +74,7 @@ describe('render', () => { mockApi = { uuid: 'control1', parentApi: { - labelPosition: new BehaviorSubject('oneLine'), + labelPosition: new BehaviorSubject('oneLine'), }, }; const controlPanel = render(); @@ -92,7 +92,7 @@ describe('render', () => { mockApi = { uuid: 'control1', parentApi: { - labelPosition: new BehaviorSubject('twoLine'), + labelPosition: new BehaviorSubject('twoLine'), }, }; const controlPanel = render(); diff --git a/src/plugins/controls/public/react_controls/control_group/get_control_group_factory.tsx b/src/plugins/controls/public/react_controls/control_group/get_control_group_factory.tsx index 6c3e8d10c3c66..77da1480eb494 100644 --- a/src/plugins/controls/public/react_controls/control_group/get_control_group_factory.tsx +++ b/src/plugins/controls/public/react_controls/control_group/get_control_group_factory.tsx @@ -11,9 +11,7 @@ import fastIsEqual from 'fast-deep-equal'; import React, { useEffect } from 'react'; import { BehaviorSubject } from 'rxjs'; -import { CoreStart } from '@kbn/core/public'; import { DataView } from '@kbn/data-views-plugin/common'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; import { i18n } from '@kbn/i18n'; import { @@ -31,11 +29,11 @@ import type { ControlGroupChainingSystem, ControlGroupRuntimeState, ControlGroupSerializedState, + ControlLabelPosition, ControlPanelsState, - ControlStyle, ParentIgnoreSettings, } from '../../../common'; -import { CONTROL_GROUP_TYPE, DEFAULT_CONTROL_STYLE } from '../../../common'; +import { CONTROL_GROUP_TYPE, DEFAULT_CONTROL_LABEL_POSITION } from '../../../common'; import { openDataControlEditor } from '../controls/data_controls/open_data_control_editor'; import { ControlGroup } from './components/control_group'; import { chaining$, controlFetch$, controlGroupFetch$ } from './control_fetch'; @@ -45,13 +43,11 @@ import { openEditControlGroupFlyout } from './open_edit_control_group_flyout'; import { initSelectionsManager } from './selections_manager'; import type { ControlGroupApi } from './types'; import { deserializeControlGroup } from './utils/serialization_utils'; +import { coreServices, dataViewsService } from '../../services/kibana_services'; const DEFAULT_CHAINING_SYSTEM = 'HIERARCHICAL'; -export const getControlGroupEmbeddableFactory = (services: { - core: CoreStart; - dataViews: DataViewsPublicPluginStart; -}) => { +export const getControlGroupEmbeddableFactory = () => { const controlGroupEmbeddableFactory: ReactEmbeddableFactory< ControlGroupSerializedState, ControlGroupRuntimeState, @@ -75,7 +71,7 @@ export const getControlGroupEmbeddableFactory = (services: { } = initialRuntimeState; const autoApplySelections$ = new BehaviorSubject(autoApplySelections); - const defaultDataViewId = await services.dataViews.getDefaultId(); + const defaultDataViewId = await dataViewsService.getDefaultId(); const lastSavedControlsState$ = new BehaviorSubject( lastSavedRuntimeState.initialChildControlState ); @@ -94,15 +90,12 @@ export const getControlGroupEmbeddableFactory = (services: { const ignoreParentSettings$ = new BehaviorSubject( ignoreParentSettings ); - const labelPosition$ = new BehaviorSubject( // TODO: Rename `ControlStyle` - initialLabelPosition ?? DEFAULT_CONTROL_STYLE // TODO: Rename `DEFAULT_CONTROL_STYLE` + const labelPosition$ = new BehaviorSubject( + initialLabelPosition ?? DEFAULT_CONTROL_LABEL_POSITION ); const allowExpensiveQueries$ = new BehaviorSubject(true); const disabledActionIds$ = new BehaviorSubject(undefined); - /** TODO: Handle loading; loading should be true if any child is loading */ - const dataLoading$ = new BehaviorSubject(false); - const unsavedChanges = initializeControlGroupUnsavedChanges( selectionsManager.applySelections, controlsManager.api.children$, @@ -122,7 +115,10 @@ export const getControlGroupEmbeddableFactory = (services: { (next: ParentIgnoreSettings | undefined) => ignoreParentSettings$.next(next), fastIsEqual, ], - labelPosition: [labelPosition$, (next: ControlStyle) => labelPosition$.next(next)], + labelPosition: [ + labelPosition$, + (next: ControlLabelPosition) => labelPosition$.next(next), + ], }, controlsManager.snapshotControlsRuntimeState, controlsManager.resetControlsUnsavedChanges, @@ -157,18 +153,13 @@ export const getControlGroupEmbeddableFactory = (services: { initialChildControlState: controlsManager.snapshotControlsRuntimeState(), }; }, - dataLoading: dataLoading$, onEdit: async () => { - openEditControlGroupFlyout( - api, - { - chainingSystem: chainingSystem$, - labelPosition: labelPosition$, - autoApplySelections: autoApplySelections$, - ignoreParentSettings: ignoreParentSettings$, - }, - { core: services.core } - ); + openEditControlGroupFlyout(api, { + chainingSystem: chainingSystem$, + labelPosition: labelPosition$, + autoApplySelections: autoApplySelections$, + ignoreParentSettings: ignoreParentSettings$, + }); }, isEditingEnabled: () => true, openAddDataControlFlyout: (settings) => { @@ -193,7 +184,6 @@ export const getControlGroupEmbeddableFactory = (services: { settings?.onSave?.(); }, controlGroupApi: api, - services, }); }, serializeState: () => { @@ -201,7 +191,7 @@ export const getControlGroupEmbeddableFactory = (services: { return { rawState: { chainingSystem: chainingSystem$.getValue(), - controlStyle: labelPosition$.getValue(), // Rename "labelPosition" to "controlStyle" + controlStyle: labelPosition$.getValue(), showApplySelections: !autoApplySelections$.getValue(), ignoreParentSettingsJSON: JSON.stringify(ignoreParentSettings$.getValue()), panelsJSON, @@ -265,10 +255,9 @@ export const getControlGroupEmbeddableFactory = (services: { /** Fetch the allowExpensiveQuries setting for the children to use if necessary */ const fetchAllowExpensiveQueries = async () => { try { - const { allowExpensiveQueries } = await services.core.http.get<{ + const { allowExpensiveQueries } = await coreServices.http.get<{ allowExpensiveQueries: boolean; - // TODO: Rename this route as part of https://github.com/elastic/kibana/issues/174961 - }>('/internal/controls/optionsList/getExpensiveQueriesSetting', { + }>('/internal/controls/getExpensiveQueriesSetting', { version: '1', }); if (!allowExpensiveQueries) { diff --git a/src/plugins/controls/public/react_controls/control_group/open_edit_control_group_flyout.tsx b/src/plugins/controls/public/react_controls/control_group/open_edit_control_group_flyout.tsx index 5e7026282123a..5e7baf1f73e5d 100644 --- a/src/plugins/controls/public/react_controls/control_group/open_edit_control_group_flyout.tsx +++ b/src/plugins/controls/public/react_controls/control_group/open_edit_control_group_flyout.tsx @@ -8,7 +8,6 @@ */ import { OverlayRef } from '@kbn/core-mount-utils-browser'; -import { CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { tracksOverlays } from '@kbn/presentation-containers'; import { apiHasParentApi } from '@kbn/presentation-publishing'; @@ -19,13 +18,11 @@ import { BehaviorSubject } from 'rxjs'; import { ControlStateManager } from '../controls/types'; import { ControlGroupEditor } from './components/control_group_editor'; import { ControlGroupApi, ControlGroupEditorState } from './types'; +import { coreServices } from '../../services/kibana_services'; export const openEditControlGroupFlyout = ( controlGroupApi: ControlGroupApi, - stateManager: ControlStateManager, - services: { - core: CoreStart; - } + stateManager: ControlStateManager ) => { /** * Duplicate all state into a new manager because we do not want to actually apply the changes @@ -50,7 +47,7 @@ export const openEditControlGroupFlyout = ( }; const onDeleteAll = (ref: OverlayRef) => { - services.core.overlays + coreServices.overlays .openConfirm( i18n.translate('controls.controlGroup.management.delete.sub', { defaultMessage: 'Controls are not recoverable once removed.', @@ -77,7 +74,7 @@ export const openEditControlGroupFlyout = ( }); }; - const overlay = services.core.overlays.openFlyout( + const overlay = coreServices.overlays.openFlyout( toMountPoint( closeOverlay(overlay)} />, { - theme: services.core.theme, - i18n: services.core.i18n, + theme: coreServices.theme, + i18n: coreServices.i18n, } ), { diff --git a/src/plugins/controls/public/react_controls/control_group/register_control_group_embeddable.ts b/src/plugins/controls/public/react_controls/control_group/register_control_group_embeddable.ts index 513633e46a875..a64faa63e8efc 100644 --- a/src/plugins/controls/public/react_controls/control_group/register_control_group_embeddable.ts +++ b/src/plugins/controls/public/react_controls/control_group/register_control_group_embeddable.ts @@ -7,23 +7,16 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { CoreSetup } from '@kbn/core/public'; import type { EmbeddableSetup } from '@kbn/embeddable-plugin/public'; -import type { ControlsPluginStartDeps } from '../../types'; import { CONTROL_GROUP_TYPE } from '../../../common'; +import { untilPluginStartServicesReady } from '../../services/kibana_services'; -export function registerControlGroupEmbeddable( - coreSetup: CoreSetup, - embeddableSetup: EmbeddableSetup -) { +export function registerControlGroupEmbeddable(embeddableSetup: EmbeddableSetup) { embeddableSetup.registerReactEmbeddableFactory(CONTROL_GROUP_TYPE, async () => { - const [{ getControlGroupEmbeddableFactory }, [coreStart, depsStart]] = await Promise.all([ + const [{ getControlGroupEmbeddableFactory }] = await Promise.all([ import('./get_control_group_factory'), - coreSetup.getStartServices(), + untilPluginStartServicesReady(), ]); - return getControlGroupEmbeddableFactory({ - core: coreStart, - dataViews: depsStart.data.dataViews, - }); + return getControlGroupEmbeddableFactory(); }); } diff --git a/src/plugins/controls/public/react_controls/control_group/types.ts b/src/plugins/controls/public/react_controls/control_group/types.ts index cb09cac975e7a..37f9f40c4079f 100644 --- a/src/plugins/controls/public/react_controls/control_group/types.ts +++ b/src/plugins/controls/public/react_controls/control_group/types.ts @@ -19,7 +19,6 @@ import { import { HasEditCapabilities, HasParentApi, - PublishesDataLoading, PublishesDisabledActionIds, PublishesFilters, PublishesTimeslice, @@ -35,8 +34,8 @@ import { ControlGroupEditorConfig, ControlGroupRuntimeState, ControlGroupSerializedState, + ControlLabelPosition, ControlPanelState, - ControlStyle, DefaultControlState, ParentIgnoreSettings, } from '../../../common'; @@ -54,7 +53,6 @@ export type ControlGroupApi = PresentationContainer & PublishesDataViews & HasSerializedChildState & HasEditCapabilities & - PublishesDataLoading & Pick, 'unsavedChanges'> & PublishesTimeslice & PublishesDisabledActionIds & @@ -62,7 +60,7 @@ export type ControlGroupApi = PresentationContainer & allowExpensiveQueries$: PublishingSubject; autoApplySelections$: PublishingSubject; ignoreParentSettings$: PublishingSubject; - labelPosition: PublishingSubject; + labelPosition: PublishingSubject; asyncResetUnsavedChanges: () => Promise; controlFetch$: (controlUuid: string) => Observable; diff --git a/src/plugins/controls/public/react_controls/control_group/utils/control_group_state_builder.ts b/src/plugins/controls/public/react_controls/control_group/utils/control_group_state_builder.ts index 91e5379416c5e..1c051e58af46f 100644 --- a/src/plugins/controls/public/react_controls/control_group/utils/control_group_state_builder.ts +++ b/src/plugins/controls/public/react_controls/control_group/utils/control_group_state_builder.ts @@ -13,11 +13,12 @@ import { OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL, TIME_SLIDER_CONTROL, + type ControlGroupRuntimeState, + type ControlPanelsState, type DefaultDataControlState, } from '../../../../common'; -import { type ControlGroupRuntimeState, type ControlPanelsState } from '../../../../common'; import type { OptionsListControlState } from '../../../../common/options_list'; -import { pluginServices } from '../../../services'; +import { dataViewsService } from '../../../services/kibana_services'; import { getDataControlFieldRegistry } from '../../controls/data_controls/data_control_editor_utils'; import type { RangesliderControlState } from '../../controls/data_controls/range_slider/types'; @@ -82,7 +83,7 @@ export const controlGroupStateBuilder = { }; async function getCompatibleControlType(dataViewId: string, fieldName: string) { - const dataView = await pluginServices.getServices().dataViews.get(dataViewId); + const dataView = await dataViewsService.get(dataViewId); const fieldRegistry = await getDataControlFieldRegistry(dataView); const field = fieldRegistry[fieldName]; if (field.compatibleControlTypes.length === 0) { diff --git a/src/plugins/controls/public/react_controls/control_group/utils/initialization_utils.ts b/src/plugins/controls/public/react_controls/control_group/utils/initialization_utils.ts index 8bd19c3d6478c..ef81b4e30b361 100644 --- a/src/plugins/controls/public/react_controls/control_group/utils/initialization_utils.ts +++ b/src/plugins/controls/public/react_controls/control_group/utils/initialization_utils.ts @@ -7,11 +7,11 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { type ControlGroupRuntimeState, DEFAULT_CONTROL_STYLE } from '../../../../common'; +import { DEFAULT_CONTROL_LABEL_POSITION, type ControlGroupRuntimeState } from '../../../../common'; export const getDefaultControlGroupRuntimeState = (): ControlGroupRuntimeState => ({ initialChildControlState: {}, - labelPosition: DEFAULT_CONTROL_STYLE, + labelPosition: DEFAULT_CONTROL_LABEL_POSITION, chainingSystem: 'HIERARCHICAL', autoApplySelections: true, ignoreParentSettings: { diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.test.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.test.tsx index 5f8f17c57bce5..8d8385d603fb3 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.test.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.test.tsx @@ -12,7 +12,6 @@ import { BehaviorSubject } from 'rxjs'; import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub'; import { stubFieldSpecMap } from '@kbn/data-views-plugin/common/field.stub'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { TimeRange } from '@kbn/es-query'; import { I18nProvider } from '@kbn/i18n-react'; import { act, fireEvent, render, RenderResult, waitFor } from '@testing-library/react'; @@ -22,6 +21,7 @@ import { DEFAULT_CONTROL_WIDTH, type DefaultDataControlState, } from '../../../../common'; +import { dataViewsService } from '../../../services/kibana_services'; import { getAllControlTypes, getControlFactory } from '../../control_factory_registry'; import type { ControlGroupApi } from '../../control_group/types'; import type { ControlFactory } from '../types'; @@ -39,7 +39,6 @@ jest.mock('../../control_factory_registry', () => ({ getControlFactory: jest.fn(), })); -const mockDataViews = dataViewPluginMocks.createStartContract(); const mockDataView = createStubDataView({ spec: { id: 'logstash-*', @@ -58,7 +57,6 @@ const mockDataView = createStubDataView({ timeFieldName: '@timestamp', }, }); -mockDataViews.get = jest.fn().mockResolvedValue(mockDataView); const dashboardApi = { timeRange$: new BehaviorSubject(undefined), @@ -82,7 +80,7 @@ describe('Data control editor', () => { controlType?: string; initialDefaultPanelTitle?: string; }) => { - mockDataViews.get = jest.fn().mockResolvedValue(mockDataView); + dataViewsService.get = jest.fn().mockResolvedValue(mockDataView); const controlEditor = render( @@ -97,13 +95,12 @@ describe('Data control editor', () => { controlId={controlId} controlType={controlType} initialDefaultPanelTitle={initialDefaultPanelTitle} - services={{ dataViews: mockDataViews }} /> ); await waitFor(() => { - expect(mockDataViews.get).toHaveBeenCalledTimes(1); + expect(dataViewsService.get).toHaveBeenCalledTimes(1); }); return controlEditor; diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.tsx index 5254fe200e97c..35e21ca3b407a 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.tsx @@ -33,7 +33,6 @@ import { EuiToolTip, } from '@elastic/eui'; import { DataViewField } from '@kbn/data-views-plugin/common'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { LazyDataViewPicker, LazyFieldPicker, @@ -46,6 +45,7 @@ import { type ControlWidth, type DefaultDataControlState, } from '../../../../common'; +import { dataViewsService } from '../../../services/kibana_services'; import { getAllControlTypes, getControlFactory } from '../../control_factory_registry'; import type { ControlGroupApi } from '../../control_group/types'; import { DataControlEditorStrings } from './data_control_constants'; @@ -67,9 +67,6 @@ export interface ControlEditorProps< controlGroupApi: ControlGroupApi; // controls must always have a parent API onCancel: (newState: Partial) => void; onSave: (newState: Partial, type: string) => void; - services: { - dataViews: DataViewsPublicPluginStart; - }; } const FieldPicker = withSuspense(LazyFieldPicker, null); @@ -151,8 +148,6 @@ export const DataControlEditor = ) => { const [editorState, setEditorState] = useState>(initialState); const [defaultPanelTitle, setDefaultPanelTitle] = useState( @@ -163,16 +158,14 @@ export const DataControlEditor = (true); const editorConfig = useMemo(() => controlGroupApi.getEditorConfig(), [controlGroupApi]); - // TODO: Maybe remove `useAsync` - see https://github.com/elastic/kibana/pull/182842#discussion_r1624909709 const { loading: dataViewListLoading, value: dataViewListItems = [], error: dataViewListError, } = useAsync(async () => { - return dataViewService.getIdsWithTitle(); + return dataViewsService.getIdsWithTitle(); }); - // TODO: Maybe remove `useAsync` - see https://github.com/elastic/kibana/pull/182842#discussion_r1624909709 const { loading: dataViewLoading, value: { selectedDataView, fieldRegistry } = { @@ -185,7 +178,7 @@ export const DataControlEditor = { return await loadFieldRegistryFromDataView(dataView); @@ -21,7 +20,6 @@ export const getDataControlFieldRegistry = memoize( (dataView: DataView) => [dataView.id, JSON.stringify(dataView.fields.getAll())].join('|') ); -/** TODO: This function is duplicated from the controls plugin to avoid exporting it */ const loadFieldRegistryFromDataView = async ( dataView: DataView ): Promise => { diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.test.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.test.tsx index 8ff50a2bc4ada..d189d0aaa1ae9 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.test.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.test.tsx @@ -7,10 +7,9 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { coreMock } from '@kbn/core/public/mocks'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import type { DataView } from '@kbn/data-views-plugin/public'; import { first, skip } from 'rxjs'; +import { dataViewsService } from '../../../services/kibana_services'; import { ControlGroupApi } from '../../control_group/types'; import { initializeDataControl } from './initialize_data_control'; @@ -21,9 +20,8 @@ describe('initializeDataControl', () => { }; const editorStateManager = {}; const controlGroupApi = {} as unknown as ControlGroupApi; - const mockDataViews = dataViewPluginMocks.createStartContract(); - // @ts-ignore - mockDataViews.get = async (id: string): Promise => { + + dataViewsService.get = async (id: string): Promise => { if (id !== 'myDataViewId') { throw new Error(`Simulated error: no data view found for id ${id}`); } @@ -40,10 +38,6 @@ describe('initializeDataControl', () => { }, } as unknown as DataView; }; - const services = { - core: coreMock.createStart(), - dataViews: mockDataViews, - }; describe('dataViewId subscription', () => { describe('no blocking errors', () => { @@ -55,8 +49,7 @@ describe('initializeDataControl', () => { 'referenceNameSuffix', dataControlState, editorStateManager, - controlGroupApi, - services + controlGroupApi ); dataControl.api.defaultPanelTitle!.pipe(skip(1), first()).subscribe(() => { @@ -90,8 +83,7 @@ describe('initializeDataControl', () => { dataViewId: 'notGonnaFindMeDataViewId', }, editorStateManager, - controlGroupApi, - services + controlGroupApi ); dataControl.api.dataViews.pipe(skip(1), first()).subscribe(() => { @@ -129,8 +121,7 @@ describe('initializeDataControl', () => { fieldName: 'notGonnaFindMeFieldName', }, editorStateManager, - controlGroupApi, - services + controlGroupApi ); dataControl.api.defaultPanelTitle!.pipe(skip(1), first()).subscribe(() => { diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.ts b/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.ts index 6b814efa1ec3d..11fb453d56350 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.ts @@ -10,19 +10,18 @@ import { isEqual } from 'lodash'; import { BehaviorSubject, combineLatest, debounceTime, first, skip, switchMap, tap } from 'rxjs'; -import { CoreStart } from '@kbn/core-lifecycle-browser'; import { DATA_VIEW_SAVED_OBJECT_TYPE, DataView, DataViewField, } from '@kbn/data-views-plugin/common'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { Filter } from '@kbn/es-query'; import { SerializedPanelState } from '@kbn/presentation-containers'; import { StateComparators } from '@kbn/presentation-publishing'; import { i18n } from '@kbn/i18n'; import type { DefaultControlState, DefaultDataControlState } from '../../../../common'; +import { dataViewsService } from '../../../services/kibana_services'; import type { ControlGroupApi } from '../../control_group/types'; import { initializeDefaultControlApi } from '../initialize_default_control_api'; import type { ControlApiInitialization, ControlStateManager } from '../types'; @@ -40,11 +39,7 @@ export const initializeDataControl = ( * responsible for managing */ editorStateManager: ControlStateManager, - controlGroupApi: ControlGroupApi, - services: { - core: CoreStart; - dataViews: DataViewsPublicPluginStart; - } + controlGroupApi: ControlGroupApi ): { api: ControlApiInitialization; cleanup: () => void; @@ -88,7 +83,7 @@ export const initializeDataControl = ( switchMap(async (currentDataViewId) => { let dataView: DataView | undefined; try { - dataView = await services.dataViews.get(currentDataViewId); + dataView = await dataViewsService.get(currentDataViewId); return { dataView }; } catch (error) { return { error }; @@ -156,7 +151,6 @@ export const initializeDataControl = ( // open the editor to get the new state openDataControlEditor({ - services, onSave: ({ type: newType, state: newState }) => { if (newType === controlType) { // apply the changes from the new state via the state manager diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/open_data_control_editor.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/open_data_control_editor.tsx index fe629555dea3c..08118702a003e 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/open_data_control_editor.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/open_data_control_editor.tsx @@ -10,14 +10,14 @@ import React from 'react'; import deepEqual from 'react-fast-compare'; -import { CoreStart, OverlayRef } from '@kbn/core/public'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import { OverlayRef } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { tracksOverlays } from '@kbn/presentation-containers'; import { apiHasParentApi } from '@kbn/presentation-publishing'; import { toMountPoint } from '@kbn/react-kibana-mount'; import type { DefaultDataControlState } from '../../../../common'; +import { coreServices } from '../../../services/kibana_services'; import type { ControlGroupApi } from '../../control_group/types'; import { DataControlEditor } from './data_control_editor'; @@ -30,7 +30,6 @@ export const openDataControlEditor = < initialDefaultPanelTitle, onSave, controlGroupApi, - services, }: { initialState: Partial; controlType?: string; @@ -38,10 +37,6 @@ export const openDataControlEditor = < initialDefaultPanelTitle?: string; onSave: ({ type, state }: { type: string; state: Partial }) => void; controlGroupApi: ControlGroupApi; - services: { - core: CoreStart; - dataViews: DataViewsPublicPluginStart; - }; }): void => { const closeOverlay = (overlayRef: OverlayRef) => { if (apiHasParentApi(controlGroupApi) && tracksOverlays(controlGroupApi.parentApi)) { @@ -55,7 +50,7 @@ export const openDataControlEditor = < closeOverlay(overlay); return; } - services.core.overlays + coreServices.overlays .openConfirm( i18n.translate('controls.controlGroup.management.discard.sub', { defaultMessage: `Changes that you've made to this control will be discarded, are you sure you want to continue?`, @@ -80,7 +75,7 @@ export const openDataControlEditor = < }); }; - const overlay = services.core.overlays.openFlyout( + const overlay = coreServices.overlays.openFlyout( toMountPoint( controlGroupApi={controlGroupApi} @@ -95,11 +90,10 @@ export const openDataControlEditor = < closeOverlay(overlay); onSave({ type: selectedControlType, state }); }} - services={{ dataViews: services.dataViews }} />, { - theme: services.core.theme, - i18n: services.core.i18n, + theme: coreServices.theme, + i18n: coreServices.i18n, } ), { diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/fetch_and_validate.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/fetch_and_validate.tsx index c2b1d7d84250e..2e2cd341e8704 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/fetch_and_validate.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/fetch_and_validate.tsx @@ -26,13 +26,11 @@ import { isValidSearch } from '../../../../../common/options_list/is_valid_searc import { OptionsListSelection } from '../../../../../common/options_list/options_list_selections'; import { ControlFetchContext } from '../../../control_group/control_fetch'; import { ControlStateManager } from '../../types'; -import { DataControlServices } from '../types'; import { OptionsListFetchCache } from './options_list_fetch_cache'; import { OptionsListComponentApi, OptionsListComponentState, OptionsListControlApi } from './types'; export function fetchAndValidate$({ api, - services, stateManager, }: { api: Pick & @@ -41,7 +39,6 @@ export function fetchAndValidate$({ loadingSuggestions$: BehaviorSubject; debouncedSearchString: Observable; }; - services: DataControlServices; stateManager: ControlStateManager< Pick > & { @@ -126,7 +123,7 @@ export function fetchAndValidate$({ const newAbortController = new AbortController(); abortController = newAbortController; try { - return await requestCache.runFetchRequest(request, newAbortController.signal, services); + return await requestCache.runFetchRequest(request, newAbortController.signal); } catch (error) { return { error }; } diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.test.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.test.tsx index 99e15a3d0f31f..20911d1cdb872 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.test.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.test.tsx @@ -9,26 +9,22 @@ import React from 'react'; -import { coreMock } from '@kbn/core/public/mocks'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { DataView } from '@kbn/data-views-plugin/common'; import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { act, render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { coreServices, dataViewsService } from '../../../../services/kibana_services'; import { getMockedBuildApi, getMockedControlGroupApi } from '../../mocks/control_mocks'; import { getOptionsListControlFactory } from './get_options_list_control_factory'; describe('Options List Control Api', () => { const uuid = 'myControl1'; const controlGroupApi = getMockedControlGroupApi(); - const mockDataViews = dataViewPluginMocks.createStartContract(); - const mockCore = coreMock.createStart(); const waitOneTick = () => act(() => new Promise((resolve) => setTimeout(resolve, 0))); - mockDataViews.get = jest.fn().mockImplementation(async (id: string): Promise => { + dataViewsService.get = jest.fn().mockImplementation(async (id: string): Promise => { if (id !== 'myDataViewId') { throw new Error(`Simulated error: no data view found for id ${id}`); } @@ -60,11 +56,7 @@ describe('Options List Control Api', () => { return stubDataView; }); - const factory = getOptionsListControlFactory({ - core: mockCore, - data: dataPluginMock.createStartContract(), - dataViews: mockDataViews, - }); + const factory = getOptionsListControlFactory(); describe('filters$', () => { test('should not set filters$ when selectedOptions is not provided', async () => { @@ -177,7 +169,7 @@ describe('Options List Control Api', () => { describe('make selection', () => { beforeAll(() => { - mockCore.http.fetch = jest.fn().mockResolvedValue({ + coreServices.http.fetch = jest.fn().mockResolvedValue({ suggestions: [ { value: 'woof', docCount: 10 }, { value: 'bark', docCount: 15 }, diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.tsx index d0c40736552ce..2a23ac9341ab9 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.tsx @@ -9,7 +9,7 @@ import fastIsEqual from 'fast-deep-equal'; import React, { useEffect } from 'react'; -import { BehaviorSubject, combineLatest, debounceTime, filter, skip } from 'rxjs'; +import { BehaviorSubject, combineLatest, debounceTime, filter, map, skip } from 'rxjs'; import { buildExistsFilter, buildPhraseFilter, buildPhrasesFilter, Filter } from '@kbn/es-query'; import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; @@ -25,7 +25,7 @@ import type { } from '../../../../../common/options_list'; import { getSelectionAsFieldType, isValidSearch } from '../../../../../common/options_list'; import { initializeDataControl } from '../initialize_data_control'; -import type { DataControlFactory, DataControlServices } from '../types'; +import type { DataControlFactory } from '../types'; import { OptionsListControl } from './components/options_list_control'; import { OptionsListEditorOptions } from './components/options_list_editor_options'; import { @@ -39,9 +39,10 @@ import { initializeOptionsListSelections } from './options_list_control_selectio import { OptionsListStrings } from './options_list_strings'; import type { OptionsListControlApi } from './types'; -export const getOptionsListControlFactory = ( - services: DataControlServices -): DataControlFactory => { +export const getOptionsListControlFactory = (): DataControlFactory< + OptionsListControlState, + OptionsListControlApi +> => { return { type: OPTIONS_LIST_CONTROL, order: 3, // should always be first, since this is the most popular control @@ -78,6 +79,7 @@ export const getOptionsListControlFactory = ( const searchStringValid$ = new BehaviorSubject(true); const requestSize$ = new BehaviorSubject(MIN_OPTIONS_LIST_REQUEST_SIZE); + const dataLoading$ = new BehaviorSubject(undefined); const availableOptions$ = new BehaviorSubject(undefined); const invalidSelections$ = new BehaviorSubject>(new Set()); const totalCardinality$ = new BehaviorSubject(0); @@ -90,8 +92,7 @@ export const getOptionsListControlFactory = ( 'optionsListDataView', initialState, { searchTechnique: searchTechnique$, singleSelect: singleSelect$ }, - controlGroupApi, - services + controlGroupApi ); const selections = initializeOptionsListSelections( @@ -115,12 +116,16 @@ export const getOptionsListControlFactory = ( /** Handle loading state; since suggestion fetching and validation are tied, only need one loading subject */ const loadingSuggestions$ = new BehaviorSubject(false); - const dataLoadingSubscription = loadingSuggestions$ + const dataLoadingSubscription = combineLatest([ + loadingSuggestions$, + dataControl.api.dataLoading, + ]) .pipe( - debounceTime(100) // debounce set loading so that it doesn't flash as the user types + debounceTime(100), // debounce set loading so that it doesn't flash as the user types + map((values) => values.some((value) => value)) ) .subscribe((isLoading) => { - dataControl.api.setDataLoading(isLoading); + dataLoading$.next(isLoading); }); /** Debounce the search string changes to reduce the number of fetch requests */ @@ -161,7 +166,6 @@ export const getOptionsListControlFactory = ( /** Fetch the suggestions and perform validation */ const loadMoreSubject = new BehaviorSubject(null); const fetchSubscription = fetchAndValidate$({ - services, api: { ...dataControl.api, loadMoreSubject, @@ -235,6 +239,7 @@ export const getOptionsListControlFactory = ( const api = buildApi( { ...dataControl.api, + dataLoading: dataLoading$, getTypeDisplayName: OptionsListStrings.control.getDisplayName, serializeState: () => { const { rawState: dataControlState, references } = dataControl.serialize(); diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/options_list_fetch_cache.ts b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/options_list_fetch_cache.ts index 548b1efebd02a..60b1463118733 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/options_list_fetch_cache.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/options_list_fetch_cache.ts @@ -20,7 +20,7 @@ import type { OptionsListResponse, OptionsListSuccessResponse, } from '../../../../../common/options_list/types'; -import type { DataControlServices } from '../types'; +import { coreServices, dataService } from '../../../../services/kibana_services'; const REQUEST_CACHE_SIZE = 50; // only store a max of 50 responses const REQUEST_CACHE_TTL = 1000 * 60; // time to live = 1 minute @@ -80,8 +80,7 @@ export class OptionsListFetchCache { public async runFetchRequest( request: OptionsListRequest, - abortSignal: AbortSignal, - services: DataControlServices + abortSignal: AbortSignal ): Promise { const requestHash = this.getRequestHash(request); @@ -90,11 +89,11 @@ export class OptionsListFetchCache { } else { const index = request.dataView.getIndexPattern(); - const timeService = services.data.query.timefilter.timefilter; + const timeService = dataService.query.timefilter.timefilter; const { query, filters, dataView, timeRange, field, ...passThroughProps } = request; const timeFilter = timeRange ? timeService.createFilter(dataView, timeRange) : undefined; const filtersToUse = [...(filters ?? []), ...(timeFilter ? [timeFilter] : [])]; - const config = getEsQueryConfig(services.core.uiSettings); + const config = getEsQueryConfig(coreServices.uiSettings); const esFilters = [buildEsQuery(dataView, query ?? [], filtersToUse ?? [], config)]; const requestBody = { @@ -105,7 +104,7 @@ export class OptionsListFetchCache { runtimeFieldMap: dataView.toSpec?.().runtimeFieldMap, }; - const result = await services.core.http.fetch( + const result = await coreServices.http.fetch( `/internal/controls/optionsList/${index}`, { version: '1', diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/register_options_list_control.ts b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/register_options_list_control.ts index 417eb42d4b1bd..b58189a75daca 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/register_options_list_control.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/register_options_list_control.ts @@ -7,21 +7,16 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { CoreSetup } from '@kbn/core/public'; -import type { ControlsPluginStartDeps } from '../../../../types'; -import { registerControlFactory } from '../../../control_factory_registry'; import { OPTIONS_LIST_CONTROL } from '../../../../../common'; +import { untilPluginStartServicesReady } from '../../../../services/kibana_services'; +import { registerControlFactory } from '../../../control_factory_registry'; -export function registerOptionsListControl(coreSetup: CoreSetup) { +export function registerOptionsListControl() { registerControlFactory(OPTIONS_LIST_CONTROL, async () => { - const [{ getOptionsListControlFactory }, [coreStart, depsStart]] = await Promise.all([ + const [{ getOptionsListControlFactory }] = await Promise.all([ import('./get_options_list_control_factory'), - coreSetup.getStartServices(), + untilPluginStartServicesReady(), ]); - return getOptionsListControlFactory({ - core: coreStart, - data: depsStart.data, - dataViews: depsStart.data.dataViews, - }); + return getOptionsListControlFactory(); }); } diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.test.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.test.tsx index 76cb52981e8c1..925ec3443849a 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.test.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.test.tsx @@ -11,13 +11,11 @@ import React from 'react'; import { of } from 'rxjs'; import { estypes } from '@elastic/elasticsearch'; -import { coreMock } from '@kbn/core/public/mocks'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { DataViewField } from '@kbn/data-views-plugin/common'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { SerializedPanelState } from '@kbn/presentation-containers'; import { fireEvent, render, waitFor } from '@testing-library/react'; +import { dataService, dataViewsService } from '../../../../services/kibana_services'; import { getMockedBuildApi, getMockedControlGroupApi } from '../../mocks/control_mocks'; import { getRangesliderControlFactory } from './get_range_slider_control_factory'; import { RangesliderControlState } from './types'; @@ -31,11 +29,10 @@ describe('RangesliderControlApi', () => { const controlGroupApi = getMockedControlGroupApi(); - const dataStartServiceMock = dataPluginMock.createStartContract(); let totalResults = DEFAULT_TOTAL_RESULTS; let min: estypes.AggregationsSingleMetricAggregateBase['value'] = DEFAULT_MIN; let max: estypes.AggregationsSingleMetricAggregateBase['value'] = DEFAULT_MAX; - dataStartServiceMock.search.searchSource.create = jest.fn().mockImplementation(() => { + dataService.search.searchSource.create = jest.fn().mockImplementation(() => { let isAggsRequest = false; return { setField: (key: string) => { @@ -54,9 +51,8 @@ describe('RangesliderControlApi', () => { }, }; }); - const mockDataViews = dataViewPluginMocks.createStartContract(); - mockDataViews.get = jest.fn().mockImplementation(async (id: string): Promise => { + dataViewsService.get = jest.fn().mockImplementation(async (id: string): Promise => { if (id !== 'myDataViewId') { throw new Error(`no data view found for id ${id}`); } @@ -82,11 +78,7 @@ describe('RangesliderControlApi', () => { } as unknown as DataView; }); - const factory = getRangesliderControlFactory({ - core: coreMock.createStart(), - data: dataStartServiceMock, - dataViews: mockDataViews, - }); + const factory = getRangesliderControlFactory(); beforeEach(() => { totalResults = DEFAULT_TOTAL_RESULTS; diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.tsx index 596206dc2f4f6..3ad3b97af7414 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.tsx @@ -16,7 +16,7 @@ import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; import { RANGE_SLIDER_CONTROL } from '../../../../../common'; import { initializeDataControl } from '../initialize_data_control'; -import type { DataControlFactory, DataControlServices } from '../types'; +import type { DataControlFactory } from '../types'; import { RangeSliderControl } from './components/range_slider_control'; import { hasNoResults$ } from './has_no_results'; import { minMax$ } from './min_max'; @@ -24,9 +24,10 @@ import { initializeRangeControlSelections } from './range_control_selections'; import { RangeSliderStrings } from './range_slider_strings'; import type { RangesliderControlApi, RangesliderControlState } from './types'; -export const getRangesliderControlFactory = ( - services: DataControlServices -): DataControlFactory => { +export const getRangesliderControlFactory = (): DataControlFactory< + RangesliderControlState, + RangesliderControlApi +> => { return { type: RANGE_SLIDER_CONTROL, getIconType: () => 'controlsHorizontal', @@ -71,8 +72,7 @@ export const getRangesliderControlFactory = ( { step: step$, }, - controlGroupApi, - services + controlGroupApi ); const selections = initializeRangeControlSelections( @@ -111,13 +111,14 @@ export const getRangesliderControlFactory = ( } ); - const dataLoadingSubscription = combineLatest([loadingMinMax$, loadingHasNoResults$]) + const dataLoadingSubscription = combineLatest([ + loadingMinMax$, + loadingHasNoResults$, + dataControl.api.dataLoading, + ]) .pipe( - map((values) => { - return values.some((value) => { - return value; - }); - }) + debounceTime(100), + map((values) => values.some((value) => value)) ) .subscribe((isLoading) => { dataLoading$.next(isLoading); @@ -138,7 +139,6 @@ export const getRangesliderControlFactory = ( const min$ = new BehaviorSubject(undefined); const minMaxSubscription = minMax$({ controlFetch$, - data: services.data, dataViews$: dataControl.api.dataViews, fieldName$: dataControl.stateManager.fieldName, setIsLoading: (isLoading: boolean) => { @@ -198,7 +198,6 @@ export const getRangesliderControlFactory = ( const selectionHasNoResults$ = new BehaviorSubject(false); const hasNotResultsSubscription = hasNoResults$({ controlFetch$, - data: services.data, dataViews$: dataControl.api.dataViews, rangeFilters$: dataControl.api.filters$, ignoreParentSettings$: controlGroupApi.ignoreParentSettings$, diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/has_no_results.ts b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/has_no_results.ts index 27676f5f7b649..24d4510b3fc22 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/has_no_results.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/has_no_results.ts @@ -8,25 +8,23 @@ */ import { estypes } from '@elastic/elasticsearch'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public'; import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; import { PublishesDataViews } from '@kbn/presentation-publishing'; -import { combineLatest, lastValueFrom, Observable, switchMap, tap } from 'rxjs'; +import { Observable, combineLatest, lastValueFrom, switchMap, tap } from 'rxjs'; +import { dataService } from '../../../../services/kibana_services'; import { ControlFetchContext } from '../../../control_group/control_fetch'; import { ControlGroupApi } from '../../../control_group/types'; import { DataControlApi } from '../types'; export function hasNoResults$({ controlFetch$, - data, dataViews$, rangeFilters$, ignoreParentSettings$, setIsLoading, }: { controlFetch$: Observable; - data: DataPublicPluginStart; dataViews$?: PublishesDataViews['dataViews']; rangeFilters$: DataControlApi['filters$']; ignoreParentSettings$: ControlGroupApi['ignoreParentSettings$']; @@ -53,7 +51,6 @@ export function hasNoResults$({ prevRequestAbortController = abortController; return await hasNoResults({ abortSignal: abortController.signal, - data, dataView, rangeFilter, ...controlFetchContext, @@ -71,7 +68,6 @@ export function hasNoResults$({ async function hasNoResults({ abortSignal, - data, dataView, filters, query, @@ -79,14 +75,13 @@ async function hasNoResults({ timeRange, }: { abortSignal: AbortSignal; - data: DataPublicPluginStart; dataView: DataView; filters?: Filter[]; query?: Query | AggregateQuery; rangeFilter: Filter; timeRange?: TimeRange; }): Promise { - const searchSource = await data.search.searchSource.create(); + const searchSource = await dataService.search.searchSource.create(); searchSource.setField('size', 0); searchSource.setField('index', dataView); // Tracking total hits accurately has a performance cost @@ -97,7 +92,7 @@ async function hasNoResults({ const allFilters = filters ? [...filters] : []; allFilters.push(rangeFilter); if (timeRange) { - const timeFilter = data.query.timefilter.timefilter.createFilter(dataView, timeRange); + const timeFilter = dataService.query.timefilter.timefilter.createFilter(dataView, timeRange); if (timeFilter) allFilters.push(timeFilter); } if (allFilters.length) { diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/min_max.ts b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/min_max.ts index d3335e182f101..8e4d5e00374af 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/min_max.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/min_max.ts @@ -8,26 +8,24 @@ */ import { estypes } from '@elastic/elasticsearch'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataView, DataViewField } from '@kbn/data-views-plugin/public'; import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; import { PublishesDataViews, PublishingSubject } from '@kbn/presentation-publishing'; -import { combineLatest, lastValueFrom, Observable, of, startWith, switchMap, tap } from 'rxjs'; import { apiPublishesReload } from '@kbn/presentation-publishing/interfaces/fetch/publishes_reload'; +import { Observable, combineLatest, lastValueFrom, of, startWith, switchMap, tap } from 'rxjs'; +import { dataService } from '../../../../services/kibana_services'; import { ControlFetchContext } from '../../../control_group/control_fetch'; import { ControlGroupApi } from '../../../control_group/types'; export function minMax$({ controlFetch$, controlGroupApi, - data, dataViews$, fieldName$, setIsLoading, }: { controlFetch$: Observable; controlGroupApi: ControlGroupApi; - data: DataPublicPluginStart; dataViews$: PublishesDataViews['dataViews']; fieldName$: PublishingSubject; setIsLoading: (isLoading: boolean) => void; @@ -60,7 +58,6 @@ export function minMax$({ prevRequestAbortController = abortController; return await getMinMax({ abortSignal: abortController.signal, - data, dataView, field: dataViewField, ...controlFetchContext, @@ -77,7 +74,6 @@ export function minMax$({ export async function getMinMax({ abortSignal, - data, dataView, field, filters, @@ -85,20 +81,19 @@ export async function getMinMax({ timeRange, }: { abortSignal: AbortSignal; - data: DataPublicPluginStart; dataView: DataView; field: DataViewField; filters?: Filter[]; query?: Query | AggregateQuery; timeRange?: TimeRange; }): Promise<{ min: number | undefined; max: number | undefined }> { - const searchSource = await data.search.searchSource.create(); + const searchSource = await dataService.search.searchSource.create(); searchSource.setField('size', 0); searchSource.setField('index', dataView); const allFilters = filters ? [...filters] : []; if (timeRange) { - const timeFilter = data.query.timefilter.timefilter.createFilter(dataView, timeRange); + const timeFilter = dataService.query.timefilter.timefilter.createFilter(dataView, timeRange); if (timeFilter) allFilters.push(timeFilter); } if (allFilters.length) { diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/register_range_slider_control.ts b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/register_range_slider_control.ts index 4f77fc3bac7e4..0e1c0fd925792 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/register_range_slider_control.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/register_range_slider_control.ts @@ -7,22 +7,17 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { CoreSetup } from '@kbn/core/public'; -import type { ControlsPluginStartDeps } from '../../../../types'; -import { registerControlFactory } from '../../../control_factory_registry'; import { RANGE_SLIDER_CONTROL } from '../../../../../common'; +import { untilPluginStartServicesReady } from '../../../../services/kibana_services'; +import { registerControlFactory } from '../../../control_factory_registry'; -export function registerRangeSliderControl(coreSetup: CoreSetup) { +export function registerRangeSliderControl() { registerControlFactory(RANGE_SLIDER_CONTROL, async () => { - const [{ getRangesliderControlFactory }, [coreStart, depsStart]] = await Promise.all([ + const [{ getRangesliderControlFactory }] = await Promise.all([ import('./get_range_slider_control_factory'), - coreSetup.getStartServices(), + untilPluginStartServicesReady(), ]); - return getRangesliderControlFactory({ - core: coreStart, - data: depsStart.data, - dataViews: depsStart.data.dataViews, - }); + return getRangesliderControlFactory(); }); } diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/types.ts b/src/plugins/controls/public/react_controls/controls/data_controls/types.ts index 9eac141642402..89912e6eabb03 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/types.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/types.ts @@ -7,10 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { CoreStart } from '@kbn/core/public'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataViewField } from '@kbn/data-views-plugin/common'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { FieldFormatConvertFunction } from '@kbn/field-formats-plugin/common'; import { HasEditCapabilities, @@ -62,12 +59,6 @@ export const isDataControlFactory = ( return typeof (factory as DataControlFactory).isFieldCompatible === 'function'; }; -export interface DataControlServices { - core: CoreStart; - data: DataPublicPluginStart; - dataViews: DataViewsPublicPluginStart; -} - interface DataControlField { field: DataViewField; compatibleControlTypes: string[]; diff --git a/src/plugins/controls/public/react_controls/controls/timeslider_control/get_time_range_meta.ts b/src/plugins/controls/public/react_controls/controls/timeslider_control/get_time_range_meta.ts index 1cccb264d19e7..5c84cfbdef508 100644 --- a/src/plugins/controls/public/react_controls/controls/timeslider_control/get_time_range_meta.ts +++ b/src/plugins/controls/public/react_controls/controls/timeslider_control/get_time_range_meta.ts @@ -9,6 +9,7 @@ import { EuiRangeTick } from '@elastic/eui'; import { TimeRange } from '@kbn/es-query'; +import { coreServices, dataService } from '../../../services/kibana_services'; import { FROM_INDEX, getStepSize, @@ -17,7 +18,6 @@ import { roundUpToNextStepSizeFactor, TO_INDEX, } from './time_utils'; -import { Services } from './types'; export interface TimeRangeMeta { format: string; @@ -29,12 +29,9 @@ export interface TimeRangeMeta { timeRangeMin: number; } -export function getTimeRangeMeta( - timeRange: TimeRange | undefined, - services: Services -): TimeRangeMeta { - const nextBounds = timeRangeToBounds(timeRange ?? getDefaultTimeRange(services), services); - const ticks = getTicks(nextBounds[FROM_INDEX], nextBounds[TO_INDEX], getTimezone(services)); +export function getTimeRangeMeta(timeRange: TimeRange | undefined): TimeRangeMeta { + const nextBounds = timeRangeToBounds(timeRange ?? getDefaultTimeRange()); + const ticks = getTicks(nextBounds[FROM_INDEX], nextBounds[TO_INDEX], getTimezone()); const { format, stepSize } = getStepSize(ticks); return { format, @@ -47,17 +44,17 @@ export function getTimeRangeMeta( }; } -export function getTimezone(services: Services) { - return services.core.uiSettings.get('dateFormat:tz', 'Browser'); +export function getTimezone() { + return coreServices.uiSettings.get('dateFormat:tz', 'Browser'); } -function getDefaultTimeRange(services: Services) { - const defaultTimeRange = services.core.uiSettings.get('timepicker:timeDefaults'); +function getDefaultTimeRange() { + const defaultTimeRange = coreServices.uiSettings.get('timepicker:timeDefaults'); return defaultTimeRange ? defaultTimeRange : { from: 'now-15m', to: 'now' }; } -function timeRangeToBounds(timeRange: TimeRange, services: Services): [number, number] { - const timeRangeBounds = services.data.query.timefilter.timefilter.calculateBounds(timeRange); +function timeRangeToBounds(timeRange: TimeRange): [number, number] { + const timeRangeBounds = dataService.query.timefilter.timefilter.calculateBounds(timeRange); return timeRangeBounds.min === undefined || timeRangeBounds.max === undefined ? [Date.now() - 1000 * 60 * 15, Date.now()] : [timeRangeBounds.min.valueOf(), timeRangeBounds.max.valueOf()]; diff --git a/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.test.tsx b/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.test.tsx index 12381ad83c407..d4b8ff6c13461 100644 --- a/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.test.tsx +++ b/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.test.tsx @@ -7,14 +7,15 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { coreMock } from '@kbn/core/public/mocks'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import React from 'react'; +import { BehaviorSubject } from 'rxjs'; + import dateMath from '@kbn/datemath'; import { TimeRange } from '@kbn/es-query'; import { StateComparators } from '@kbn/presentation-publishing'; import { fireEvent, render } from '@testing-library/react'; -import React from 'react'; -import { BehaviorSubject } from 'rxjs'; + +import { dataService } from '../../../services/kibana_services'; import { getMockedControlGroupApi } from '../mocks/control_mocks'; import { ControlApiRegistration } from '../types'; import { getTimesliderControlFactory } from './get_timeslider_control_factory'; @@ -28,18 +29,14 @@ describe('TimesliderControlApi', () => { }; const controlGroupApi = getMockedControlGroupApi(dashboardApi); - const dataStartServiceMock = dataPluginMock.createStartContract(); - dataStartServiceMock.query.timefilter.timefilter.calculateBounds = (timeRange: TimeRange) => { + dataService.query.timefilter.timefilter.calculateBounds = (timeRange: TimeRange) => { const now = new Date(); return { min: dateMath.parse(timeRange.from, { forceNow: now }), max: dateMath.parse(timeRange.to, { roundUp: true, forceNow: now }), }; }; - const factory = getTimesliderControlFactory({ - core: coreMock.createStart(), - data: dataStartServiceMock, - }); + const factory = getTimesliderControlFactory(); let comparators: StateComparators | undefined; function buildApiMock( api: ControlApiRegistration, diff --git a/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.tsx b/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.tsx index 20baf8fb545e1..cfc8e50bee1b5 100644 --- a/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.tsx +++ b/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.tsx @@ -36,22 +36,23 @@ import { roundDownToNextStepSizeFactor, roundUpToNextStepSizeFactor, } from './time_utils'; -import { Services, Timeslice, TimesliderControlApi, TimesliderControlState } from './types'; +import { Timeslice, TimesliderControlApi, TimesliderControlState } from './types'; const displayName = i18n.translate('controls.timesliderControl.displayName', { defaultMessage: 'Time slider', }); -export const getTimesliderControlFactory = ( - services: Services -): ControlFactory => { +export const getTimesliderControlFactory = (): ControlFactory< + TimesliderControlState, + TimesliderControlApi +> => { return { type: TIME_SLIDER_CONTROL, getIconType: () => 'search', getDisplayName: () => displayName, buildControl: async (initialState, buildApi, uuid, controlGroupApi) => { const { timeRangeMeta$, formatDate, cleanupTimeRangeSubscription } = - initTimeRangeSubscription(controlGroupApi, services); + initTimeRangeSubscription(controlGroupApi); const timeslice$ = new BehaviorSubject<[number, number] | undefined>(undefined); const isAnchored$ = new BehaviorSubject(initialState.isAnchored); const isPopoverOpen$ = new BehaviorSubject(false); diff --git a/src/plugins/controls/public/react_controls/controls/timeslider_control/init_time_range_subscription.ts b/src/plugins/controls/public/react_controls/controls/timeslider_control/init_time_range_subscription.ts index 7b4a2deb9f0d1..7934e9deaa9b4 100644 --- a/src/plugins/controls/public/react_controls/controls/timeslider_control/init_time_range_subscription.ts +++ b/src/plugins/controls/public/react_controls/controls/timeslider_control/init_time_range_subscription.ts @@ -14,26 +14,23 @@ import moment from 'moment'; import { BehaviorSubject, skip } from 'rxjs'; import { getTimeRangeMeta, getTimezone, TimeRangeMeta } from './get_time_range_meta'; import { getMomentTimezone } from './time_utils'; -import { Services } from './types'; -export function initTimeRangeSubscription(controlGroupApi: unknown, services: Services) { +export function initTimeRangeSubscription(controlGroupApi: unknown) { const timeRange$ = apiHasParentApi(controlGroupApi) && apiPublishesTimeRange(controlGroupApi.parentApi) ? controlGroupApi.parentApi.timeRange$ : new BehaviorSubject(undefined); - const timeRangeMeta$ = new BehaviorSubject( - getTimeRangeMeta(timeRange$.value, services) - ); + const timeRangeMeta$ = new BehaviorSubject(getTimeRangeMeta(timeRange$.value)); const timeRangeSubscription = timeRange$.pipe(skip(1)).subscribe((timeRange) => { - timeRangeMeta$.next(getTimeRangeMeta(timeRange, services)); + timeRangeMeta$.next(getTimeRangeMeta(timeRange)); }); return { timeRangeMeta$, formatDate: (epoch: number) => { return moment - .tz(epoch, getMomentTimezone(getTimezone(services))) + .tz(epoch, getMomentTimezone(getTimezone())) .locale(i18n.getLocale()) .format(timeRangeMeta$.value.format); }, diff --git a/src/plugins/controls/public/react_controls/controls/timeslider_control/register_timeslider_control.ts b/src/plugins/controls/public/react_controls/controls/timeslider_control/register_timeslider_control.ts index 8fbf23305820f..338a52631c931 100644 --- a/src/plugins/controls/public/react_controls/controls/timeslider_control/register_timeslider_control.ts +++ b/src/plugins/controls/public/react_controls/controls/timeslider_control/register_timeslider_control.ts @@ -7,20 +7,16 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { CoreSetup } from '@kbn/core/public'; -import type { ControlsPluginStartDeps } from '../../../types'; -import { registerControlFactory } from '../../control_factory_registry'; import { TIME_SLIDER_CONTROL } from '../../../../common'; +import { untilPluginStartServicesReady } from '../../../services/kibana_services'; +import { registerControlFactory } from '../../control_factory_registry'; -export function registerTimeSliderControl(coreSetup: CoreSetup) { +export function registerTimeSliderControl() { registerControlFactory(TIME_SLIDER_CONTROL, async () => { - const [{ getTimesliderControlFactory }, [coreStart, depsStart]] = await Promise.all([ + const [{ getTimesliderControlFactory }] = await Promise.all([ import('./get_timeslider_control_factory'), - coreSetup.getStartServices(), + untilPluginStartServicesReady(), ]); - return getTimesliderControlFactory({ - core: coreStart, - data: depsStart.data, - }); + return getTimesliderControlFactory(); }); } diff --git a/src/plugins/controls/public/react_controls/controls/timeslider_control/types.ts b/src/plugins/controls/public/react_controls/controls/timeslider_control/types.ts index 702d02ae9accc..634e0351e77eb 100644 --- a/src/plugins/controls/public/react_controls/controls/timeslider_control/types.ts +++ b/src/plugins/controls/public/react_controls/controls/timeslider_control/types.ts @@ -7,8 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { CoreStart } from '@kbn/core/public'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { PublishesPanelTitle, PublishesTimeslice } from '@kbn/presentation-publishing'; import type { DefaultControlState } from '../../../../common'; import type { DefaultControlApi } from '../types'; @@ -25,8 +23,3 @@ export interface TimesliderControlState extends DefaultControlState { export type TimesliderControlApi = DefaultControlApi & Pick & PublishesTimeslice; - -export interface Services { - core: CoreStart; - data: DataPublicPluginStart; -} diff --git a/src/plugins/controls/public/react_controls/controls/types.ts b/src/plugins/controls/public/react_controls/controls/types.ts index 85045f8bd70ba..ce4ad9f194fa3 100644 --- a/src/plugins/controls/public/react_controls/controls/types.ts +++ b/src/plugins/controls/public/react_controls/controls/types.ts @@ -40,15 +40,15 @@ export type DefaultControlApi = PublishesDataLoading & HasType & HasUniqueId & HasParentApi & { - // Can not use HasSerializableState interface - // HasSerializableState types serializeState as function returning 'MaybePromise' - // Controls serializeState is sync - serializeState: () => SerializedPanelState; - /** TODO: Make these non-public as part of https://github.com/elastic/kibana/issues/174961 */ setDataLoading: (loading: boolean) => void; setBlockingError: (error: Error | undefined) => void; grow: PublishingSubject; width: PublishingSubject; + + // Can not use HasSerializableState interface + // HasSerializableState types serializeState as function returning 'MaybePromise' + // Controls serializeState is sync + serializeState: () => SerializedPanelState; }; export type ControlApiRegistration = Omit< @@ -62,7 +62,6 @@ export type ControlApiInitialization; -// TODO: Move this to the Control plugin's setup contract export interface ControlFactory< State extends DefaultControlState = DefaultControlState, ControlApi extends DefaultControlApi = DefaultControlApi diff --git a/src/plugins/controls/public/react_controls/external_api/control_group_renderer.test.tsx b/src/plugins/controls/public/react_controls/external_api/control_group_renderer.test.tsx index 58269308f1846..e034ca817908e 100644 --- a/src/plugins/controls/public/react_controls/external_api/control_group_renderer.test.tsx +++ b/src/plugins/controls/public/react_controls/external_api/control_group_renderer.test.tsx @@ -9,26 +9,22 @@ import React from 'react'; -import { coreMock } from '@kbn/core/public/mocks'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; import { Filter } from '@kbn/es-query'; import { PublishesUnifiedSearch, PublishingSubject } from '@kbn/presentation-publishing'; import { act, render, waitFor } from '@testing-library/react'; import { ControlGroupRendererApi } from '.'; +import { CONTROL_GROUP_TYPE } from '../..'; import { getControlGroupEmbeddableFactory } from '../control_group/get_control_group_factory'; import { ControlGroupRenderer, ControlGroupRendererProps } from './control_group_renderer'; -import { CONTROL_GROUP_TYPE } from '../..'; type ParentApiType = PublishesUnifiedSearch & { unifiedSearchFilters$?: PublishingSubject; }; describe('control group renderer', () => { - const core = coreMock.createStart(); - const dataViews = dataViewPluginMocks.createStartContract(); - const factory = getControlGroupEmbeddableFactory({ core, dataViews }); + const factory = getControlGroupEmbeddableFactory(); const buildControlGroupSpy = jest.spyOn(factory, 'buildEmbeddable'); const mountControlGroupRenderer = async ( diff --git a/src/plugins/controls/public/services/controls/controls.stub.ts b/src/plugins/controls/public/services/controls/controls.stub.ts deleted file mode 100644 index 2e182998c9071..0000000000000 --- a/src/plugins/controls/public/services/controls/controls.stub.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlFactory, DefaultControlApi } from '../../react_controls/controls/types'; -import { ControlsServiceType } from './types'; - -export type ControlsServiceFactory = PluginServiceFactory; -export const controlsServiceFactory = () => getStubControlsService(); - -export const getStubControlsService = () => { - const controlsFactoriesMap: { [key: string]: ControlFactory } = {}; - - const mockRegisterControlFactory = async < - State extends object = object, - ApiType extends DefaultControlApi = DefaultControlApi - >( - controlType: string, - getFactory: () => Promise> - ) => { - controlsFactoriesMap[controlType] = (await getFactory()) as ControlFactory; - }; - - const mockGetControlFactory = < - State extends object = object, - ApiType extends DefaultControlApi = DefaultControlApi - >( - type: string - ) => { - return controlsFactoriesMap[type] as ControlFactory; - }; - - const getAllControlTypes = () => Object.keys(controlsFactoriesMap); - - return { - registerControlFactory: mockRegisterControlFactory, - getControlFactory: mockGetControlFactory, - getAllControlTypes, - }; -}; diff --git a/src/plugins/controls/public/services/controls/controls_service.ts b/src/plugins/controls/public/services/controls/controls_service.ts deleted file mode 100644 index c794c056a4f8d..0000000000000 --- a/src/plugins/controls/public/services/controls/controls_service.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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - getAllControlTypes, - getControlFactory, - registerControlFactory, -} from '../../react_controls/control_factory_registry'; -import { ControlsServiceType } from './types'; - -export const controlsServiceFactory = () => controlsService; - -// export controls service directly for use in plugin setup lifecycle -export const controlsService: ControlsServiceType = { - registerControlFactory, - getControlFactory, - getAllControlTypes, -}; diff --git a/src/plugins/controls/public/services/controls/types.ts b/src/plugins/controls/public/services/controls/types.ts deleted file mode 100644 index d9011819d815c..0000000000000 --- a/src/plugins/controls/public/services/controls/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { - getAllControlTypes, - getControlFactory, - registerControlFactory, -} from '../../react_controls/control_factory_registry'; - -export type ControlsServiceFactory = PluginServiceFactory; - -export interface ControlsServiceType { - registerControlFactory: typeof registerControlFactory; - getControlFactory: typeof getControlFactory; - getAllControlTypes: typeof getAllControlTypes; -} diff --git a/src/plugins/controls/public/services/core/core.stub.ts b/src/plugins/controls/public/services/core/core.stub.ts deleted file mode 100644 index 052ddf46129e2..0000000000000 --- a/src/plugins/controls/public/services/core/core.stub.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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { analyticsServiceMock, coreMock, themeServiceMock } from '@kbn/core/public/mocks'; -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsCoreService } from './types'; - -export type CoreServiceFactory = PluginServiceFactory; - -export const coreServiceFactory: CoreServiceFactory = () => { - const corePluginMock = coreMock.createStart(); - return { - analytics: analyticsServiceMock.createAnalyticsServiceStart(), - theme: themeServiceMock.createSetupContract(), - i18n: corePluginMock.i18n, - notifications: corePluginMock.notifications, - }; -}; diff --git a/src/plugins/controls/public/services/core/core_service.ts b/src/plugins/controls/public/services/core/core_service.ts deleted file mode 100644 index 090784c32d806..0000000000000 --- a/src/plugins/controls/public/services/core/core_service.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsCoreService } from './types'; -import { ControlsPluginStartDeps } from '../../types'; - -export type CoreServiceFactory = KibanaPluginServiceFactory< - ControlsCoreService, - ControlsPluginStartDeps ->; - -export const coreServiceFactory: CoreServiceFactory = ({ coreStart }) => { - const { analytics, theme, i18n, notifications } = coreStart; - - return { - analytics, - theme, - i18n, - notifications, - }; -}; diff --git a/src/plugins/controls/public/services/core/types.ts b/src/plugins/controls/public/services/core/types.ts deleted file mode 100644 index 9424a490b3f3d..0000000000000 --- a/src/plugins/controls/public/services/core/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { CoreStart } from '@kbn/core/public'; - -export interface ControlsCoreService { - analytics: CoreStart['analytics']; - i18n: CoreStart['i18n']; - theme: CoreStart['theme']; - notifications: CoreStart['notifications']; -} diff --git a/src/plugins/controls/public/services/data/data.stub.ts b/src/plugins/controls/public/services/data/data.stub.ts deleted file mode 100644 index 5cd50a68768ef..0000000000000 --- a/src/plugins/controls/public/services/data/data.stub.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { of } from 'rxjs'; -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { ControlsDataService } from './types'; - -export type DataServiceFactory = PluginServiceFactory; -export const dataServiceFactory: DataServiceFactory = () => ({ - query: {} as unknown as DataPublicPluginStart['query'], - searchSource: { - create: () => ({ - setField: () => {}, - fetch$: () => - of({ - rawResponse: { aggregations: { minAgg: { value: 0 }, maxAgg: { value: 1000 } } }, - }), - }), - } as unknown as DataPublicPluginStart['search']['searchSource'], - timefilter: { - createFilter: () => {}, - } as unknown as DataPublicPluginStart['query']['timefilter']['timefilter'], - fetchFieldRange: () => Promise.resolve({ min: 0, max: 100 }), -}); diff --git a/src/plugins/controls/public/services/data/data_service.ts b/src/plugins/controls/public/services/data/data_service.ts deleted file mode 100644 index 84a36c2775dcd..0000000000000 --- a/src/plugins/controls/public/services/data/data_service.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsPluginStartDeps } from '../../types'; -import { ControlsDataService } from './types'; - -export type DataServiceFactory = KibanaPluginServiceFactory< - ControlsDataService, - ControlsPluginStartDeps ->; - -export const dataServiceFactory: DataServiceFactory = ({ startPlugins }) => { - const { - data: { query: queryPlugin, search }, - } = startPlugins; - - return { - query: queryPlugin, - searchSource: search.searchSource, - timefilter: queryPlugin.timefilter.timefilter, - }; -}; diff --git a/src/plugins/controls/public/services/data/types.ts b/src/plugins/controls/public/services/data/types.ts deleted file mode 100644 index 68a7553ee08dc..0000000000000 --- a/src/plugins/controls/public/services/data/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -export interface ControlsDataService { - query: DataPublicPluginStart['query']; - searchSource: DataPublicPluginStart['search']['searchSource']; - timefilter: DataPublicPluginStart['query']['timefilter']['timefilter']; -} diff --git a/src/plugins/controls/public/services/data_views/data_views.stub.ts b/src/plugins/controls/public/services/data_views/data_views.stub.ts deleted file mode 100644 index f2ffcf5f08a9d..0000000000000 --- a/src/plugins/controls/public/services/data_views/data_views.stub.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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; -import { DataView } from '@kbn/data-views-plugin/common'; -import { ControlsDataViewsService } from './types'; - -export type DataViewsServiceFactory = PluginServiceFactory; - -let currentDataView: DataView | undefined; -export const injectStorybookDataView = (dataView?: DataView) => (currentDataView = dataView); - -export const dataViewsServiceFactory: DataViewsServiceFactory = () => ({ - get: ((dataViewId) => - new Promise((resolve, reject) => - setTimeout(() => { - if (!currentDataView) { - reject( - new Error( - 'mock DataViews service currentDataView is undefined, call injectStorybookDataView to set' - ) - ); - } else if (currentDataView.id === dataViewId) { - resolve(currentDataView); - } else { - reject( - new Error( - `mock DataViews service currentDataView.id: ${currentDataView.id} does not match requested dataViewId: ${dataViewId}` - ) - ); - } - }, 100) - ) as unknown) as DataViewsPublicPluginStart['get'], - getIdsWithTitle: (() => - new Promise((resolve) => - setTimeout(() => { - const idsWithTitle: Array<{ id: string | undefined; title: string }> = []; - if (currentDataView) { - idsWithTitle.push({ id: currentDataView.id, title: currentDataView.title }); - } - resolve(idsWithTitle); - }, 100) - ) as unknown) as DataViewsPublicPluginStart['getIdsWithTitle'], - getDefaultId: () => Promise.resolve(currentDataView?.id ?? null), -}); diff --git a/src/plugins/controls/public/services/data_views/data_views_service.ts b/src/plugins/controls/public/services/data_views/data_views_service.ts deleted file mode 100644 index 4ad4c0b8d2241..0000000000000 --- a/src/plugins/controls/public/services/data_views/data_views_service.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsPluginStartDeps } from '../../types'; -import { ControlsDataViewsService } from './types'; - -export type DataViewsServiceFactory = KibanaPluginServiceFactory< - ControlsDataViewsService, - ControlsPluginStartDeps ->; - -export const dataViewsServiceFactory: DataViewsServiceFactory = ({ startPlugins }) => { - const { - dataViews: { get, getIdsWithTitle, getDefaultId }, - } = startPlugins; - - return { - get, - getDefaultId, - getIdsWithTitle, - }; -}; diff --git a/src/plugins/controls/public/services/data_views/types.ts b/src/plugins/controls/public/services/data_views/types.ts deleted file mode 100644 index a204af439634e..0000000000000 --- a/src/plugins/controls/public/services/data_views/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; - -export interface ControlsDataViewsService { - get: DataViewsPublicPluginStart['get']; - getDefaultId: DataViewsPublicPluginStart['getDefaultId']; - getIdsWithTitle: DataViewsPublicPluginStart['getIdsWithTitle']; -} diff --git a/src/plugins/controls/public/services/embeddable/embeddable.stub.ts b/src/plugins/controls/public/services/embeddable/embeddable.stub.ts deleted file mode 100644 index 5f75b4e7b2d14..0000000000000 --- a/src/plugins/controls/public/services/embeddable/embeddable.stub.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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; -import { ControlsEmbeddableService } from './types'; - -export type EmbeddableServiceFactory = PluginServiceFactory; -export const embeddableServiceFactory: EmbeddableServiceFactory = () => { - const { doStart } = embeddablePluginMock.createInstance(); - const start = doStart(); - return { getEmbeddableFactory: start.getEmbeddableFactory }; -}; diff --git a/src/plugins/controls/public/services/embeddable/embeddable_service.ts b/src/plugins/controls/public/services/embeddable/embeddable_service.ts deleted file mode 100644 index 79c556f69c058..0000000000000 --- a/src/plugins/controls/public/services/embeddable/embeddable_service.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsEmbeddableService } from './types'; -import { ControlsPluginStartDeps } from '../../types'; - -export type EmbeddableServiceFactory = KibanaPluginServiceFactory< - ControlsEmbeddableService, - ControlsPluginStartDeps ->; - -export const embeddableServiceFactory: EmbeddableServiceFactory = ({ startPlugins }) => { - return { - getEmbeddableFactory: startPlugins.embeddable.getEmbeddableFactory, - }; -}; diff --git a/src/plugins/controls/public/services/embeddable/types.ts b/src/plugins/controls/public/services/embeddable/types.ts deleted file mode 100644 index 917f03fb55e9b..0000000000000 --- a/src/plugins/controls/public/services/embeddable/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { EmbeddableStart } from '@kbn/embeddable-plugin/public'; - -export interface ControlsEmbeddableService { - getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; -} diff --git a/src/plugins/controls/public/services/http/http.stub.ts b/src/plugins/controls/public/services/http/http.stub.ts deleted file mode 100644 index ead893559b488..0000000000000 --- a/src/plugins/controls/public/services/http/http.stub.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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { HttpResponse } from '@kbn/core/public'; -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsHTTPService } from './types'; - -type HttpServiceFactory = PluginServiceFactory; - -export const httpServiceFactory: HttpServiceFactory = () => ({ - get: async () => ({} as unknown as HttpResponse), - fetch: async () => ({} as unknown as HttpResponse), -}); diff --git a/src/plugins/controls/public/services/http/http_service.ts b/src/plugins/controls/public/services/http/http_service.ts deleted file mode 100644 index fac813ea92272..0000000000000 --- a/src/plugins/controls/public/services/http/http_service.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsHTTPService } from './types'; -import { ControlsPluginStartDeps } from '../../types'; - -export type HttpServiceFactory = KibanaPluginServiceFactory< - ControlsHTTPService, - ControlsPluginStartDeps ->; -export const httpServiceFactory: HttpServiceFactory = ({ coreStart }) => { - const { - http: { get, fetch }, - } = coreStart; - - return { - get, - fetch, - }; -}; diff --git a/src/plugins/controls/public/services/http/types.ts b/src/plugins/controls/public/services/http/types.ts deleted file mode 100644 index 0072bc0dacff0..0000000000000 --- a/src/plugins/controls/public/services/http/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { CoreSetup } from '@kbn/core/public'; - -export interface ControlsHTTPService { - get: CoreSetup['http']['get']; - fetch: CoreSetup['http']['fetch']; -} diff --git a/src/plugins/controls/public/services/index.ts b/src/plugins/controls/public/services/index.ts deleted file mode 100644 index a7cc0715e08d9..0000000000000 --- a/src/plugins/controls/public/services/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export { pluginServices } from './plugin_services'; diff --git a/src/plugins/controls/public/services/kibana_services.ts b/src/plugins/controls/public/services/kibana_services.ts new file mode 100644 index 0000000000000..b9e5fadcecaa0 --- /dev/null +++ b/src/plugins/controls/public/services/kibana_services.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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { BehaviorSubject } from 'rxjs'; + +import { CoreStart } from '@kbn/core/public'; +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; + +import { ControlsPluginStartDeps } from '../types'; + +export let coreServices: CoreStart; +export let dataService: DataPublicPluginStart; +export let dataViewsService: DataViewsPublicPluginStart; + +const servicesReady$ = new BehaviorSubject(false); + +export const setKibanaServices = (kibanaCore: CoreStart, deps: ControlsPluginStartDeps) => { + coreServices = kibanaCore; + dataService = deps.data; + dataViewsService = deps.dataViews; + + servicesReady$.next(true); +}; + +export const untilPluginStartServicesReady = () => { + if (servicesReady$.value) return Promise.resolve(); + return new Promise((resolve) => { + const subscription = servicesReady$.subscribe((isInitialized) => { + if (isInitialized) { + subscription.unsubscribe(); + resolve(); + } + }); + }); +}; diff --git a/src/plugins/controls/public/services/mocks.ts b/src/plugins/controls/public/services/mocks.ts new file mode 100644 index 0000000000000..231323c37a12a --- /dev/null +++ b/src/plugins/controls/public/services/mocks.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { coreMock } from '@kbn/core/public/mocks'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; + +import { setKibanaServices } from './kibana_services'; + +export const setStubKibanaServices = () => { + setKibanaServices(coreMock.createStart(), { + data: dataPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), + uiActions: uiActionsPluginMock.createStartContract(), + }); +}; diff --git a/src/plugins/controls/public/services/overlays/overlays.stub.ts b/src/plugins/controls/public/services/overlays/overlays.stub.ts deleted file mode 100644 index d7938f5a8a3ce..0000000000000 --- a/src/plugins/controls/public/services/overlays/overlays.stub.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - MountPoint, - OverlayFlyoutOpenOptions, - OverlayModalConfirmOptions, - OverlayRef, -} from '@kbn/core/public'; -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsOverlaysService } from './types'; - -type OverlaysServiceFactory = PluginServiceFactory; - -class StubRef implements OverlayRef { - public readonly onClose: Promise = Promise.resolve(); - - public close(): Promise { - return this.onClose; - } -} - -export const overlaysServiceFactory: OverlaysServiceFactory = () => ({ - openFlyout: (mount: MountPoint, options?: OverlayFlyoutOpenOptions) => new StubRef(), - openConfirm: (message: MountPoint | string, options?: OverlayModalConfirmOptions) => - Promise.resolve(true), -}); diff --git a/src/plugins/controls/public/services/overlays/overlays_service.ts b/src/plugins/controls/public/services/overlays/overlays_service.ts deleted file mode 100644 index b8e7309e4eb31..0000000000000 --- a/src/plugins/controls/public/services/overlays/overlays_service.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsPluginStartDeps } from '../../types'; -import { ControlsOverlaysService } from './types'; - -export type OverlaysServiceFactory = KibanaPluginServiceFactory< - ControlsOverlaysService, - ControlsPluginStartDeps ->; -export const overlaysServiceFactory: OverlaysServiceFactory = ({ coreStart }) => { - const { - overlays: { openFlyout, openConfirm }, - } = coreStart; - - return { - openFlyout, - openConfirm, - }; -}; diff --git a/src/plugins/controls/public/services/overlays/types.ts b/src/plugins/controls/public/services/overlays/types.ts deleted file mode 100644 index 7cb5fd2975549..0000000000000 --- a/src/plugins/controls/public/services/overlays/types.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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - MountPoint, - OverlayFlyoutOpenOptions, - OverlayModalConfirmOptions, - OverlayRef, -} from '@kbn/core/public'; - -export interface ControlsOverlaysService { - openFlyout(mount: MountPoint, options?: OverlayFlyoutOpenOptions): OverlayRef; - openConfirm(message: MountPoint | string, options?: OverlayModalConfirmOptions): Promise; -} diff --git a/src/plugins/controls/public/services/plugin_services.stub.ts b/src/plugins/controls/public/services/plugin_services.stub.ts deleted file mode 100644 index 1e7b3982f88cc..0000000000000 --- a/src/plugins/controls/public/services/plugin_services.stub.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - PluginServiceProvider, - PluginServiceProviders, - PluginServiceRegistry, - PluginServices, -} from '@kbn/presentation-util-plugin/public'; - -import { ControlsPluginStart } from '../types'; -import { ControlsServices } from './types'; - -import { controlsServiceFactory } from './controls/controls.stub'; -import { coreServiceFactory } from './core/core.stub'; -import { dataServiceFactory } from './data/data.stub'; -import { dataViewsServiceFactory } from './data_views/data_views.stub'; -import { embeddableServiceFactory } from './embeddable/embeddable.stub'; -import { httpServiceFactory } from './http/http.stub'; -import { overlaysServiceFactory } from './overlays/overlays.stub'; -import { settingsServiceFactory } from './settings/settings.stub'; -import { unifiedSearchServiceFactory } from './unified_search/unified_search.stub'; -import { storageServiceFactory } from './storage/storage_service.stub'; - -export const providers: PluginServiceProviders = { - embeddable: new PluginServiceProvider(embeddableServiceFactory), - controls: new PluginServiceProvider(controlsServiceFactory), - data: new PluginServiceProvider(dataServiceFactory), - dataViews: new PluginServiceProvider(dataViewsServiceFactory), - http: new PluginServiceProvider(httpServiceFactory), - overlays: new PluginServiceProvider(overlaysServiceFactory), - settings: new PluginServiceProvider(settingsServiceFactory), - core: new PluginServiceProvider(coreServiceFactory), - storage: new PluginServiceProvider(storageServiceFactory), - unifiedSearch: new PluginServiceProvider(unifiedSearchServiceFactory), -}; - -export const pluginServices = new PluginServices(); - -export const registry = new PluginServiceRegistry(providers); - -export const getStubPluginServices = (): ControlsPluginStart => { - pluginServices.setRegistry(registry.start({})); - return { - getControlFactory: pluginServices.getServices().controls.getControlFactory, - getAllControlTypes: pluginServices.getServices().controls.getAllControlTypes, - }; -}; diff --git a/src/plugins/controls/public/services/plugin_services.ts b/src/plugins/controls/public/services/plugin_services.ts deleted file mode 100644 index d0d2552871173..0000000000000 --- a/src/plugins/controls/public/services/plugin_services.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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - KibanaPluginServiceParams, - PluginServiceProvider, - PluginServiceProviders, - PluginServiceRegistry, - PluginServices, -} from '@kbn/presentation-util-plugin/public'; -import { ControlsPluginStartDeps } from '../types'; -import { ControlsServices } from './types'; - -import { controlsServiceFactory } from './controls/controls_service'; -import { coreServiceFactory } from './core/core_service'; -import { dataServiceFactory } from './data/data_service'; -import { dataViewsServiceFactory } from './data_views/data_views_service'; -import { embeddableServiceFactory } from './embeddable/embeddable_service'; -import { httpServiceFactory } from './http/http_service'; -import { overlaysServiceFactory } from './overlays/overlays_service'; -import { settingsServiceFactory } from './settings/settings_service'; -import { controlsStorageServiceFactory } from './storage/storage_service'; -import { unifiedSearchServiceFactory } from './unified_search/unified_search_service'; - -export const providers: PluginServiceProviders< - ControlsServices, - KibanaPluginServiceParams -> = { - controls: new PluginServiceProvider(controlsServiceFactory), - data: new PluginServiceProvider(dataServiceFactory), - dataViews: new PluginServiceProvider(dataViewsServiceFactory), - embeddable: new PluginServiceProvider(embeddableServiceFactory), - http: new PluginServiceProvider(httpServiceFactory), - overlays: new PluginServiceProvider(overlaysServiceFactory), - settings: new PluginServiceProvider(settingsServiceFactory), - storage: new PluginServiceProvider(controlsStorageServiceFactory), - core: new PluginServiceProvider(coreServiceFactory), - unifiedSearch: new PluginServiceProvider(unifiedSearchServiceFactory), -}; - -export const pluginServices = new PluginServices(); - -export const registry = new PluginServiceRegistry< - ControlsServices, - KibanaPluginServiceParams ->(providers); diff --git a/src/plugins/controls/public/services/settings/settings.stub.ts b/src/plugins/controls/public/services/settings/settings.stub.ts deleted file mode 100644 index 6ee5bdce4f2f4..0000000000000 --- a/src/plugins/controls/public/services/settings/settings.stub.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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsSettingsService } from './types'; - -export type SettingsServiceFactory = PluginServiceFactory; -export const settingsServiceFactory: SettingsServiceFactory = () => ({ - getTimezone: () => 'Browser', - getDateFormat: () => 'MMM D, YYYY @ HH:mm:ss.SSS', - getDefaultTimeRange: () => ({ from: 'now-15m', to: 'now' }), -}); diff --git a/src/plugins/controls/public/services/settings/settings_service.ts b/src/plugins/controls/public/services/settings/settings_service.ts deleted file mode 100644 index e5f8751503a67..0000000000000 --- a/src/plugins/controls/public/services/settings/settings_service.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsSettingsService } from './types'; -import { ControlsPluginStartDeps } from '../../types'; - -export type SettingsServiceFactory = KibanaPluginServiceFactory< - ControlsSettingsService, - ControlsPluginStartDeps ->; - -export const settingsServiceFactory: SettingsServiceFactory = ({ coreStart }) => { - return { - getDateFormat: () => { - return coreStart.uiSettings.get('dateFormat', 'MMM D, YYYY @ HH:mm:ss.SSS'); - }, - getTimezone: () => { - return coreStart.uiSettings.get('dateFormat:tz', 'Browser'); - }, - getDefaultTimeRange: () => { - const defaultTimeRange = coreStart.uiSettings.get('timepicker:timeDefaults'); - return defaultTimeRange ? defaultTimeRange : { from: 'now-15m', to: 'now' }; - }, - }; -}; diff --git a/src/plugins/controls/public/services/settings/types.ts b/src/plugins/controls/public/services/settings/types.ts deleted file mode 100644 index f8b29adeb85a7..0000000000000 --- a/src/plugins/controls/public/services/settings/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import type { TimeRange } from '@kbn/es-query'; - -export interface ControlsSettingsService { - getTimezone: () => string; - getDateFormat: () => string; - getDefaultTimeRange: () => TimeRange; -} diff --git a/src/plugins/controls/public/services/storage/storage_service.stub.ts b/src/plugins/controls/public/services/storage/storage_service.stub.ts deleted file mode 100644 index 389d804b51bb0..0000000000000 --- a/src/plugins/controls/public/services/storage/storage_service.stub.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", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsStorageService } from './types'; - -type StorageServiceFactory = PluginServiceFactory; - -export const storageServiceFactory: StorageServiceFactory = () => { - return { - getShowInvalidSelectionWarning: () => false, - setShowInvalidSelectionWarning: (value: boolean) => null, - }; -}; diff --git a/src/plugins/controls/public/services/storage/storage_service.ts b/src/plugins/controls/public/services/storage/storage_service.ts deleted file mode 100644 index 9f0403c505e74..0000000000000 --- a/src/plugins/controls/public/services/storage/storage_service.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { Storage } from '@kbn/kibana-utils-plugin/public'; -import { ControlsStorageService } from './types'; - -const STORAGE_KEY = 'controls:showInvalidSelectionWarning'; - -class StorageService implements ControlsStorageService { - private storage: Storage; - - constructor() { - this.storage = new Storage(localStorage); - } - - getShowInvalidSelectionWarning = () => { - return this.storage.get(STORAGE_KEY); - }; - - setShowInvalidSelectionWarning = (value: boolean) => { - this.storage.set(STORAGE_KEY, value); - }; -} - -export const controlsStorageServiceFactory = () => new StorageService(); diff --git a/src/plugins/controls/public/services/storage/types.ts b/src/plugins/controls/public/services/storage/types.ts deleted file mode 100644 index 2d1e8b08e8364..0000000000000 --- a/src/plugins/controls/public/services/storage/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export interface ControlsStorageService { - getShowInvalidSelectionWarning: () => boolean; - setShowInvalidSelectionWarning: (value: boolean) => void; -} diff --git a/src/plugins/controls/public/services/types.ts b/src/plugins/controls/public/services/types.ts deleted file mode 100644 index c38ad6b64fac6..0000000000000 --- a/src/plugins/controls/public/services/types.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { ControlsServiceType } from './controls/types'; -import { ControlsCoreService } from './core/types'; -import { ControlsDataService } from './data/types'; -import { ControlsDataViewsService } from './data_views/types'; -import { ControlsEmbeddableService } from './embeddable/types'; -import { ControlsHTTPService } from './http/types'; -import { ControlsOverlaysService } from './overlays/types'; -import { ControlsSettingsService } from './settings/types'; -import { ControlsStorageService } from './storage/types'; -import { ControlsUnifiedSearchService } from './unified_search/types'; - -export interface ControlsServices { - // dependency services - dataViews: ControlsDataViewsService; - overlays: ControlsOverlaysService; - embeddable: ControlsEmbeddableService; - data: ControlsDataService; - unifiedSearch: ControlsUnifiedSearchService; - http: ControlsHTTPService; - settings: ControlsSettingsService; - core: ControlsCoreService; - - // controls plugin's own services - controls: ControlsServiceType; - storage: ControlsStorageService; -} diff --git a/src/plugins/controls/public/services/unified_search/types.ts b/src/plugins/controls/public/services/unified_search/types.ts deleted file mode 100644 index 28aa8e05feb2d..0000000000000 --- a/src/plugins/controls/public/services/unified_search/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; - -export interface ControlsUnifiedSearchService { - autocomplete: UnifiedSearchPublicPluginStart['autocomplete']; -} diff --git a/src/plugins/controls/public/services/unified_search/unified_search.stub.ts b/src/plugins/controls/public/services/unified_search/unified_search.stub.ts deleted file mode 100644 index c7b1959c8bbc0..0000000000000 --- a/src/plugins/controls/public/services/unified_search/unified_search.stub.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { DataViewField } from '@kbn/data-views-plugin/common'; -import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; - -import { ControlsUnifiedSearchService } from './types'; - -let valueSuggestionMethod = ({ field, query }: { field: DataViewField; query: string }) => - Promise.resolve(['storybook', 'default', 'values']); -export const replaceValueSuggestionMethod = ( - newMethod: ({ field, query }: { field: DataViewField; query: string }) => Promise -) => (valueSuggestionMethod = newMethod); - -export type UnifiedSearchServiceFactory = PluginServiceFactory; -export const unifiedSearchServiceFactory: UnifiedSearchServiceFactory = () => ({ - autocomplete: { - getValueSuggestions: valueSuggestionMethod, - } as unknown as UnifiedSearchPublicPluginStart['autocomplete'], -}); diff --git a/src/plugins/controls/public/services/unified_search/unified_search_service.ts b/src/plugins/controls/public/services/unified_search/unified_search_service.ts deleted file mode 100644 index 45d10cfa9ac7d..0000000000000 --- a/src/plugins/controls/public/services/unified_search/unified_search_service.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsPluginStartDeps } from '../../types'; -import { ControlsUnifiedSearchService } from './types'; - -export type UnifiedSearchServiceFactory = KibanaPluginServiceFactory< - ControlsUnifiedSearchService, - ControlsPluginStartDeps ->; - -export const unifiedSearchServiceFactory: UnifiedSearchServiceFactory = ({ startPlugins }) => { - const { - unifiedSearch: { autocomplete }, - } = startPlugins; - - return { - autocomplete, - }; -}; diff --git a/src/plugins/controls/public/types.ts b/src/plugins/controls/public/types.ts index 2ecbd38763603..bed3260bb4401 100644 --- a/src/plugins/controls/public/types.ts +++ b/src/plugins/controls/public/types.ts @@ -9,11 +9,8 @@ import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; -import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public'; +import { EmbeddableSetup } from '@kbn/embeddable-plugin/public'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; - -import { ControlsServiceType } from './services/controls/types'; export interface CanClearSelections { clearSelections: () => void; @@ -26,22 +23,11 @@ export const isClearableControl = (control: unknown): control is CanClearSelecti /** * Plugin types */ -export interface ControlsPluginSetup { - registerControlFactory: ControlsServiceType['registerControlFactory']; -} - -export interface ControlsPluginStart { - getControlFactory: ControlsServiceType['getControlFactory']; - getAllControlTypes: ControlsServiceType['getAllControlTypes']; -} - export interface ControlsPluginSetupDeps { embeddable: EmbeddableSetup; } export interface ControlsPluginStartDeps { uiActions: UiActionsStart; - embeddable: EmbeddableStart; data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; - unifiedSearch: UnifiedSearchPublicPluginStart; } diff --git a/src/plugins/controls/server/control_group/control_group_persistence.ts b/src/plugins/controls/server/control_group/control_group_persistence.ts index eb63b28ccecba..e90aa850c6d1a 100644 --- a/src/plugins/controls/server/control_group/control_group_persistence.ts +++ b/src/plugins/controls/server/control_group/control_group_persistence.ts @@ -10,7 +10,7 @@ import { SerializableRecord } from '@kbn/utility-types'; import { - DEFAULT_CONTROL_STYLE, + DEFAULT_CONTROL_LABEL_POSITION, type ControlGroupRuntimeState, type ControlGroupSerializedState, type ControlPanelState, @@ -19,7 +19,7 @@ import { export const getDefaultControlGroupState = (): SerializableControlGroupState => ({ panels: {}, - labelPosition: DEFAULT_CONTROL_STYLE, + labelPosition: DEFAULT_CONTROL_LABEL_POSITION, chainingSystem: 'HIERARCHICAL', autoApplySelections: true, ignoreParentSettings: { diff --git a/src/plugins/controls/server/options_list/options_list_cluster_settings_route.ts b/src/plugins/controls/server/options_list/options_list_cluster_settings_route.ts index f51b9a5b5b62c..04b0aaa3e6f78 100644 --- a/src/plugins/controls/server/options_list/options_list_cluster_settings_route.ts +++ b/src/plugins/controls/server/options_list/options_list_cluster_settings_route.ts @@ -15,7 +15,7 @@ export const setupOptionsListClusterSettingsRoute = ({ http }: CoreSetup) => { router.versioned .get({ access: 'internal', - path: '/internal/controls/optionsList/getExpensiveQueriesSetting', + path: '/internal/controls/getExpensiveQueriesSetting', }) .addVersion( { diff --git a/src/plugins/controls/tsconfig.json b/src/plugins/controls/tsconfig.json index fc9d6572ccf2f..abcafa291358e 100644 --- a/src/plugins/controls/tsconfig.json +++ b/src/plugins/controls/tsconfig.json @@ -35,7 +35,6 @@ "@kbn/presentation-containers", "@kbn/presentation-publishing", "@kbn/content-management-utils", - "@kbn/core-lifecycle-browser", "@kbn/field-formats-plugin", "@kbn/presentation-panel-plugin", "@kbn/shared-ux-utility" From c5ff79a1a5119db39beacadbaedbc0ebdeac6f68 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 20 Sep 2024 02:38:58 +1000 Subject: [PATCH 18/19] [8.x] [ES|QL] Enhances the inline documentation experience (#192156) (#193444) # Backport This will backport the following commits from `main` to `8.x`: - [[ES|QL] Enhances the inline documentation experience (#192156)](https://github.com/elastic/kibana/pull/192156) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Stratoula Kalafateli --- .../steps/esql_generate_function_metadata.sh | 2 +- .../index.ts | 9 +- .../package.json | 9 +- .../scripts/generate_esql_docs.ts | 10 +- .../setup_tests.ts | 11 + ...language_documentation_popover.stories.tsx | 2 +- .../src/components/as_flyout/index.test.tsx | 95 ++ .../src/components/as_flyout/index.tsx | 117 ++ .../src/components/as_inline/index.test.tsx | 97 ++ .../src/components/as_inline/index.tsx | 82 ++ .../{ => as_popover}/documentation.scss | 0 .../index.tsx} | 6 +- .../popover_content.test.tsx} | 2 +- .../popover_content.tsx} | 45 +- .../shared/documentation_content.tsx | 82 ++ .../shared/documentation_navigation.tsx | 109 ++ .../src/components/shared/index.ts | 11 + .../sections}/esql_documentation_sections.tsx | 270 ++--- .../generated/aggregation_functions.tsx | 140 +-- .../sections}/generated/scalar_functions.tsx | 1043 ++++++----------- .../src/sections/index.ts | 44 + .../src/types.ts | 17 + .../src/utils/get_filtered_groups.test.tsx | 85 ++ .../src/utils/get_filtered_groups.ts | 41 + .../tsconfig.json | 1 + packages/kbn-text-based-editor/README.md | 1 - packages/kbn-text-based-editor/package.json | 7 +- .../src/editor_footer/index.tsx | 115 +- packages/kbn-text-based-editor/src/helpers.ts | 37 - .../src/text_based_languages_editor.test.tsx | 30 +- .../src/text_based_languages_editor.tsx | 5 +- packages/kbn-text-based-editor/src/types.ts | 3 + packages/kbn-text-based-editor/tsconfig.json | 1 - .../esql_menu_popover.test.tsx | 2 +- .../query_string_input/esql_menu_popover.tsx | 85 +- src/plugins/unified_search/tsconfig.json | 3 +- .../index_data_visualizer_esql.tsx | 1 + .../expression/esql_query_expression.tsx | 1 + .../translations/translations/fr-FR.json | 476 ++++---- .../translations/translations/ja-JP.json | 476 ++++---- .../translations/translations/zh-CN.json | 476 ++++---- 41 files changed, 2199 insertions(+), 1850 deletions(-) rename packages/{kbn-text-based-editor => kbn-language-documentation-popover}/scripts/generate_esql_docs.ts (92%) create mode 100644 packages/kbn-language-documentation-popover/setup_tests.ts create mode 100644 packages/kbn-language-documentation-popover/src/components/as_flyout/index.test.tsx create mode 100644 packages/kbn-language-documentation-popover/src/components/as_flyout/index.tsx create mode 100644 packages/kbn-language-documentation-popover/src/components/as_inline/index.test.tsx create mode 100644 packages/kbn-language-documentation-popover/src/components/as_inline/index.tsx rename packages/kbn-language-documentation-popover/src/components/{ => as_popover}/documentation.scss (100%) rename packages/kbn-language-documentation-popover/src/components/{documentation_popover.tsx => as_popover/index.tsx} (95%) rename packages/kbn-language-documentation-popover/src/components/{documentation_content.test.tsx => as_popover/popover_content.test.tsx} (97%) rename packages/kbn-language-documentation-popover/src/components/{documentation_content.tsx => as_popover/popover_content.tsx} (84%) create mode 100644 packages/kbn-language-documentation-popover/src/components/shared/documentation_content.tsx create mode 100644 packages/kbn-language-documentation-popover/src/components/shared/documentation_navigation.tsx create mode 100644 packages/kbn-language-documentation-popover/src/components/shared/index.ts rename packages/{kbn-text-based-editor/src/inline_documentation => kbn-language-documentation-popover/src/sections}/esql_documentation_sections.tsx (79%) rename packages/{kbn-text-based-editor/src/inline_documentation => kbn-language-documentation-popover/src/sections}/generated/aggregation_functions.tsx (77%) rename packages/{kbn-text-based-editor/src/inline_documentation => kbn-language-documentation-popover/src/sections}/generated/scalar_functions.tsx (77%) create mode 100644 packages/kbn-language-documentation-popover/src/sections/index.ts create mode 100644 packages/kbn-language-documentation-popover/src/types.ts create mode 100644 packages/kbn-language-documentation-popover/src/utils/get_filtered_groups.test.tsx create mode 100644 packages/kbn-language-documentation-popover/src/utils/get_filtered_groups.ts diff --git a/.buildkite/scripts/steps/esql_generate_function_metadata.sh b/.buildkite/scripts/steps/esql_generate_function_metadata.sh index 837a962b3c42b..07de4bc9bd04c 100755 --- a/.buildkite/scripts/steps/esql_generate_function_metadata.sh +++ b/.buildkite/scripts/steps/esql_generate_function_metadata.sh @@ -2,7 +2,7 @@ set -euo pipefail VALIDATION_PACKAGE_DIR="packages/kbn-esql-validation-autocomplete" -EDITOR_PACKAGE_DIR="packages/kbn-text-based-editor" +EDITOR_PACKAGE_DIR="packages/kbn-language-documentation-popover" GIT_SCOPE="$VALIDATION_PACKAGE_DIR/**/* $EDITOR_PACKAGE_DIR/**/*" report_main_step () { diff --git a/packages/kbn-language-documentation-popover/index.ts b/packages/kbn-language-documentation-popover/index.ts index a417382773a54..c12962fdf22b2 100644 --- a/packages/kbn-language-documentation-popover/index.ts +++ b/packages/kbn-language-documentation-popover/index.ts @@ -6,7 +6,8 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ - -export { LanguageDocumentationPopover } from './src/components/documentation_popover'; -export { LanguageDocumentationPopoverContent } from './src/components/documentation_content'; -export type { LanguageDocumentationSections } from './src/components/documentation_content'; +export { LanguageDocumentationPopover } from './src/components/as_popover'; +export { LanguageDocumentationPopoverContent } from './src/components/as_popover/popover_content'; +export { LanguageDocumentationFlyout } from './src/components/as_flyout'; +export { LanguageDocumentationInline } from './src/components/as_inline'; +export type { LanguageDocumentationSections } from './src/types'; diff --git a/packages/kbn-language-documentation-popover/package.json b/packages/kbn-language-documentation-popover/package.json index a756b25061b64..002c3c4ee51b3 100644 --- a/packages/kbn-language-documentation-popover/package.json +++ b/packages/kbn-language-documentation-popover/package.json @@ -5,5 +5,10 @@ "private": true, "sideEffects": [ "*.scss" - ] -} \ No newline at end of file + ], + "scripts": { + "make:docs": "ts-node --transpileOnly scripts/generate_esql_docs.ts", + "postmake:docs": "yarn run lint:fix", + "lint:fix": "cd ../.. && node ./scripts/eslint --fix ./packages/kbn-language-documentation-popover/src/sections/generated" + } +} diff --git a/packages/kbn-text-based-editor/scripts/generate_esql_docs.ts b/packages/kbn-language-documentation-popover/scripts/generate_esql_docs.ts similarity index 92% rename from packages/kbn-text-based-editor/scripts/generate_esql_docs.ts rename to packages/kbn-language-documentation-popover/scripts/generate_esql_docs.ts index 8a38908e2b211..4fad23e2e25f2 100644 --- a/packages/kbn-text-based-editor/scripts/generate_esql_docs.ts +++ b/packages/kbn-language-documentation-popover/scripts/generate_esql_docs.ts @@ -11,18 +11,18 @@ import * as recast from 'recast'; const n = recast.types.namedTypes; import fs from 'fs'; import path from 'path'; -import { functions } from '../src/inline_documentation/generated/scalar_functions'; +import { functions } from '../src/sections/generated/scalar_functions'; (function () { const pathToElasticsearch = process.argv[2]; const { scalarFunctions, aggregationFunctions } = loadFunctionDocs(pathToElasticsearch); writeFunctionDocs( scalarFunctions, - path.join(__dirname, '../src/inline_documentation/generated/scalar_functions.tsx') + path.join(__dirname, '../src/sections/generated/scalar_functions.tsx') ); writeFunctionDocs( aggregationFunctions, - path.join(__dirname, '../src/inline_documentation/generated/aggregation_functions.tsx') + path.join(__dirname, '../src/sections/generated/aggregation_functions.tsx') ); })(); @@ -86,7 +86,7 @@ function writeFunctionDocs(functionDocs: Map, pathToDocsFile: st // Do not edit manually... automatically generated by scripts/generate_esql_docs.ts { label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.${name}', + 'languageDocumentationPopover.documentationESQL.${name}', { defaultMessage: '${name.toUpperCase()}', } @@ -97,7 +97,7 @@ function writeFunctionDocs(functionDocs: Map, pathToDocsFile: st readOnly enableSoftLineBreaks markdownContent={i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.${name}.markdown', + 'languageDocumentationPopover.documentationESQL.${name}.markdown', { defaultMessage: \`${docWithoutLinks.replaceAll('`', '\\`')}\`, description: diff --git a/packages/kbn-language-documentation-popover/setup_tests.ts b/packages/kbn-language-documentation-popover/setup_tests.ts new file mode 100644 index 0000000000000..5ebc6d3dac1ca --- /dev/null +++ b/packages/kbn-language-documentation-popover/setup_tests.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import '@testing-library/jest-dom'; diff --git a/packages/kbn-language-documentation-popover/src/__stories__/language_documentation_popover.stories.tsx b/packages/kbn-language-documentation-popover/src/__stories__/language_documentation_popover.stories.tsx index 658f1fe81129c..06ace5c916201 100644 --- a/packages/kbn-language-documentation-popover/src/__stories__/language_documentation_popover.stories.tsx +++ b/packages/kbn-language-documentation-popover/src/__stories__/language_documentation_popover.stories.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; -import { LanguageDocumentationPopover } from '../components/documentation_popover'; +import { LanguageDocumentationPopover } from '../components/as_popover'; const sections = { groups: [ diff --git a/packages/kbn-language-documentation-popover/src/components/as_flyout/index.test.tsx b/packages/kbn-language-documentation-popover/src/components/as_flyout/index.test.tsx new file mode 100644 index 0000000000000..5dd66386c4188 --- /dev/null +++ b/packages/kbn-language-documentation-popover/src/components/as_flyout/index.test.tsx @@ -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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { screen, render, fireEvent, waitFor } from '@testing-library/react'; +import { LanguageDocumentationFlyout } from '.'; + +jest.mock('../../sections', () => { + const module = jest.requireActual('../../sections'); + return { + ...module, + getESQLDocsSections: () => ({ + groups: [ + { + label: 'Section one', + description: 'Section 1 description', + items: [], + }, + { + label: 'Section two', + items: [ + { + label: 'Section two item 1', + description: 'Section two item 1 description', + }, + { + label: 'Section two item 2', + description: 'Section two item 2 description', + }, + ], + }, + { + label: 'Section three', + items: [ + { + label: 'Section three item 1', + description: 'Section three item 1 description', + }, + { + label: 'Section three item 2', + description: 'Section three item 2 description', + }, + ], + }, + ], + initialSection: Here is the initial section, + }), + }; +}); + +describe('###Documentation flyout component', () => { + const renderFlyout = (linkToDocumentation?: string) => { + return render( + + ); + }; + it('has a header element for navigation through the sections', () => { + renderFlyout(); + expect(screen.getByTestId('language-documentation-navigation-search')).toBeInTheDocument(); + expect(screen.getByTestId('language-documentation-navigation-dropdown')).toBeInTheDocument(); + expect(screen.queryByTestId('language-documentation-navigation-link')).not.toBeInTheDocument(); + }); + + it('has a link if linkToDocumentation prop is given', () => { + renderFlyout('meow'); + expect(screen.getByTestId('language-documentation-navigation-link')).toBeInTheDocument(); + }); + + it('contains the two last sections', async () => { + renderFlyout(); + await waitFor(() => { + expect(screen.getByText('Section two')).toBeInTheDocument(); + expect(screen.getByText('Section three')).toBeInTheDocument(); + }); + }); + + it('contains the correct section if user updates the search input', async () => { + renderFlyout(); + const input = screen.getByTestId('language-documentation-navigation-search'); + fireEvent.change(input, { target: { value: 'two' } }); + await waitFor(() => { + expect(screen.getByText('Section two')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/kbn-language-documentation-popover/src/components/as_flyout/index.tsx b/packages/kbn-language-documentation-popover/src/components/as_flyout/index.tsx new file mode 100644 index 0000000000000..0a617165f7661 --- /dev/null +++ b/packages/kbn-language-documentation-popover/src/components/as_flyout/index.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import React, { useCallback, useEffect, useState, useRef, useMemo } from 'react'; +import { + EuiFlyout, + useEuiTheme, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { getFilteredGroups } from '../../utils/get_filtered_groups'; +import { DocumentationMainContent, DocumentationNavigation } from '../shared'; +import { getESQLDocsSections } from '../../sections'; +import type { LanguageDocumentationSections } from '../../types'; + +interface DocumentationFlyoutProps { + isHelpMenuOpen: boolean; + onHelpMenuVisibilityChange: (status: boolean) => void; + searchInDescription?: boolean; + linkToDocumentation?: string; +} + +function DocumentationFlyout({ + searchInDescription, + linkToDocumentation, + isHelpMenuOpen, + onHelpMenuVisibilityChange, +}: DocumentationFlyoutProps) { + const [documentationSections, setDocumentationSections] = + useState(); + + const { euiTheme } = useEuiTheme(); + const DEFAULT_WIDTH = euiTheme.base * 34; + + const [selectedSection, setSelectedSection] = useState(); + const [searchText, setSearchText] = useState(''); + + const scrollTargets = useRef>({}); + + const onNavigationChange = useCallback((selectedOptions) => { + setSelectedSection(selectedOptions.length ? selectedOptions[0].label : undefined); + if (selectedOptions.length) { + const scrollToElement = scrollTargets.current[selectedOptions[0].label]; + scrollToElement.scrollIntoView(); + } + }, []); + + useEffect(() => { + onHelpMenuVisibilityChange(isHelpMenuOpen ?? false); + }, [isHelpMenuOpen, onHelpMenuVisibilityChange]); + + useEffect(() => { + async function getDocumentation() { + const sections = await getESQLDocsSections(); + setDocumentationSections(sections); + } + if (!documentationSections) { + getDocumentation(); + } + }, [documentationSections]); + + const filteredGroups = useMemo(() => { + return getFilteredGroups(searchText, searchInDescription, documentationSections, 1); + }, [documentationSections, searchText, searchInDescription]); + + return ( + <> + {isHelpMenuOpen && ( + onHelpMenuVisibilityChange(false)} + aria-labelledby="esqlInlineDocumentationFlyout" + type="push" + size={DEFAULT_WIDTH} + paddingSize="m" + > + + +

+ {i18n.translate('languageDocumentationPopover.documentationFlyoutTitle', { + defaultMessage: 'ES|QL quick reference', + })} +

+
+ + +
+ + + +
+ )} + + ); +} + +export const LanguageDocumentationFlyout = React.memo(DocumentationFlyout); diff --git a/packages/kbn-language-documentation-popover/src/components/as_inline/index.test.tsx b/packages/kbn-language-documentation-popover/src/components/as_inline/index.test.tsx new file mode 100644 index 0000000000000..4ba873614f9b2 --- /dev/null +++ b/packages/kbn-language-documentation-popover/src/components/as_inline/index.test.tsx @@ -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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { screen, render, fireEvent, waitFor } from '@testing-library/react'; +import { Markdown } from '@kbn/shared-ux-markdown'; +import { LanguageDocumentationInline } from '.'; + +const mockMarkDownDescription = () => ( + +); + +jest.mock('../../sections', () => { + const module = jest.requireActual('../../sections'); + return { + ...module, + getESQLDocsSections: () => ({ + groups: [ + { + label: 'Section one', + description: 'Section 1 description', + items: [], + }, + { + label: 'Section two', + items: [ + { + label: 'Section two item 1', + description: 'Section two item 1 description', + }, + { + label: 'Section two item 2', + description: 'Section two item 2 description', + }, + ], + }, + { + label: 'Section three', + items: [ + { + label: 'Section three item 1', + description: mockMarkDownDescription(), + }, + { + label: 'Section three item 2', + description: 'Section three item 2 description', + }, + ], + }, + ], + initialSection: Here is the initial section, + }), + }; +}); + +describe('###Documentation flyout component', () => { + const renderInlineComponent = (searchInDescription = false) => { + return render(); + }; + it('has a header element for navigation through the sections', () => { + renderInlineComponent(); + expect(screen.getByTestId('language-documentation-navigation-search')).toBeInTheDocument(); + expect(screen.getByTestId('language-documentation-navigation-dropdown')).toBeInTheDocument(); + }); + + it('contains the two last sections', async () => { + renderInlineComponent(); + await waitFor(() => { + expect(screen.getByText('Section two')).toBeInTheDocument(); + expect(screen.getByText('Section three')).toBeInTheDocument(); + }); + }); + + it('contains the correct section if user updates the search input', async () => { + renderInlineComponent(); + const input = screen.getByTestId('language-documentation-navigation-search'); + fireEvent.change(input, { target: { value: 'two' } }); + await waitFor(() => { + expect(screen.getByText('Section two')).toBeInTheDocument(); + }); + }); + + it('contains the correct section if user updates the search input with a text that exist in the description', async () => { + renderInlineComponent(true); + const input = screen.getByTestId('language-documentation-navigation-search'); + fireEvent.change(input, { target: { value: 'blah' } }); + await waitFor(() => { + expect(screen.getByText('Section three')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/kbn-language-documentation-popover/src/components/as_inline/index.tsx b/packages/kbn-language-documentation-popover/src/components/as_inline/index.tsx new file mode 100644 index 0000000000000..dcc860aa70db2 --- /dev/null +++ b/packages/kbn-language-documentation-popover/src/components/as_inline/index.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import React, { useCallback, useState, useRef, useMemo, useEffect } from 'react'; +import { css } from '@emotion/react'; +import { useEuiTheme, euiScrollBarStyles, EuiSpacer } from '@elastic/eui'; +import { getFilteredGroups } from '../../utils/get_filtered_groups'; +import { DocumentationMainContent, DocumentationNavigation } from '../shared'; +import type { LanguageDocumentationSections } from '../../types'; +import { getESQLDocsSections } from '../../sections'; + +interface DocumentationInlineProps { + searchInDescription?: boolean; +} + +const MAX_HEIGHT = 250; + +function DocumentationInline({ searchInDescription }: DocumentationInlineProps) { + const theme = useEuiTheme(); + const [documentationSections, setDocumentationSections] = + useState(); + const scrollBarStyles = euiScrollBarStyles(theme); + const [selectedSection, setSelectedSection] = useState(); + const [searchText, setSearchText] = useState(''); + + const scrollTargets = useRef>({}); + + useEffect(() => { + async function getDocumentation() { + const sections = await getESQLDocsSections(); + setDocumentationSections(sections); + } + if (!documentationSections) { + getDocumentation(); + } + }, [documentationSections]); + + const filteredGroups = useMemo(() => { + return getFilteredGroups(searchText, searchInDescription, documentationSections, 1); + }, [documentationSections, searchText, searchInDescription]); + + const onNavigationChange = useCallback((selectedOptions) => { + setSelectedSection(selectedOptions.length ? selectedOptions[0].label : undefined); + if (selectedOptions.length) { + const scrollToElement = scrollTargets.current[selectedOptions[0].label]; + scrollToElement.scrollIntoView(); + } + }, []); + + return ( +
+ + + +
+ ); +} + +export const LanguageDocumentationInline = React.memo(DocumentationInline); diff --git a/packages/kbn-language-documentation-popover/src/components/documentation.scss b/packages/kbn-language-documentation-popover/src/components/as_popover/documentation.scss similarity index 100% rename from packages/kbn-language-documentation-popover/src/components/documentation.scss rename to packages/kbn-language-documentation-popover/src/components/as_popover/documentation.scss diff --git a/packages/kbn-language-documentation-popover/src/components/documentation_popover.tsx b/packages/kbn-language-documentation-popover/src/components/as_popover/index.tsx similarity index 95% rename from packages/kbn-language-documentation-popover/src/components/documentation_popover.tsx rename to packages/kbn-language-documentation-popover/src/components/as_popover/index.tsx index 265e3304f7d90..9a1432e938f04 100644 --- a/packages/kbn-language-documentation-popover/src/components/documentation_popover.tsx +++ b/packages/kbn-language-documentation-popover/src/components/as_popover/index.tsx @@ -16,10 +16,8 @@ import { EuiButtonIconProps, EuiOutsideClickDetector, } from '@elastic/eui'; -import { - type LanguageDocumentationSections, - LanguageDocumentationPopoverContent, -} from './documentation_content'; +import { LanguageDocumentationPopoverContent } from './popover_content'; +import type { LanguageDocumentationSections } from '../../types'; interface DocumentationPopoverProps { language: string; diff --git a/packages/kbn-language-documentation-popover/src/components/documentation_content.test.tsx b/packages/kbn-language-documentation-popover/src/components/as_popover/popover_content.test.tsx similarity index 97% rename from packages/kbn-language-documentation-popover/src/components/documentation_content.test.tsx rename to packages/kbn-language-documentation-popover/src/components/as_popover/popover_content.test.tsx index d5acdad75dae4..a7a4bce8c2cd5 100644 --- a/packages/kbn-language-documentation-popover/src/components/documentation_content.test.tsx +++ b/packages/kbn-language-documentation-popover/src/components/as_popover/popover_content.test.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { mountWithIntl, findTestSubject } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; import { Markdown } from '@kbn/shared-ux-markdown'; -import { LanguageDocumentationPopoverContent } from './documentation_content'; +import { LanguageDocumentationPopoverContent } from './popover_content'; describe('###Documentation popover content', () => { const sections = { diff --git a/packages/kbn-language-documentation-popover/src/components/documentation_content.tsx b/packages/kbn-language-documentation-popover/src/components/as_popover/popover_content.tsx similarity index 84% rename from packages/kbn-language-documentation-popover/src/components/documentation_content.tsx rename to packages/kbn-language-documentation-popover/src/components/as_popover/popover_content.tsx index d3fedfee9f1eb..ec622eccf6044 100644 --- a/packages/kbn-language-documentation-popover/src/components/documentation_content.tsx +++ b/packages/kbn-language-documentation-popover/src/components/as_popover/popover_content.tsx @@ -6,8 +6,7 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ - -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, @@ -22,19 +21,11 @@ import { EuiSpacer, EuiLink, } from '@elastic/eui'; -import { elementToString } from '../utils/element_to_string'; +import { getFilteredGroups } from '../../utils/get_filtered_groups'; +import type { LanguageDocumentationSections } from '../../types'; import './documentation.scss'; -export interface LanguageDocumentationSections { - groups: Array<{ - label: string; - description?: string; - items: Array<{ label: string; description?: JSX.Element }>; - }>; - initialSection: JSX.Element; -} - interface DocumentationProps { language: string; sections?: LanguageDocumentationSections; @@ -61,29 +52,9 @@ function DocumentationContent({ const [searchText, setSearchText] = useState(''); - const normalizedSearchText = searchText.trim().toLocaleLowerCase(); - - const filteredGroups = sections?.groups - .map((group) => { - const items = group.items.filter((helpItem) => { - return ( - !normalizedSearchText || - helpItem.label.toLocaleLowerCase().includes(normalizedSearchText) || - // Converting the JSX element to a string first - (searchInDescription && - elementToString(helpItem.description) - ?.toLocaleLowerCase() - .includes(normalizedSearchText)) - ); - }); - return { ...group, items }; - }) - .filter((group) => { - if (group.items.length > 0 || !normalizedSearchText) { - return true; - } - return group.label.toLocaleLowerCase().includes(normalizedSearchText); - }); + const filteredGroups = useMemo(() => { + return getFilteredGroups(searchText, searchInDescription, sections); + }, [sections, searchText, searchInDescription]); return ( <> @@ -158,12 +129,12 @@ function DocumentationContent({ - {helpGroup.items.length ? ( + {helpGroup.options.length ? ( <> - {helpGroup.items.map((helpItem) => { + {helpGroup.options.map((helpItem) => { return ( ; + filteredGroups?: Array<{ + label: string; + description?: string; + options: Array<{ label: string; description?: JSX.Element | undefined }>; + }>; + sections?: LanguageDocumentationSections; +} + +function DocumentationContent({ + searchText, + scrollTargets, + filteredGroups, + sections, +}: DocumentationContentProps) { + return ( + <> + + + {!searchText && ( +
{ + if (el && sections?.groups?.length) { + scrollTargets.current[sections.groups[0].label] = el; + } + }} + > + {sections?.initialSection} +
+ )} + {filteredGroups?.map((helpGroup, index) => { + return ( +
{ + if (el) { + scrollTargets.current[helpGroup.label] = el; + } + }} + > +

{helpGroup.label}

+ +

{helpGroup.description}

+ + {filteredGroups?.[index].options.map((helpItem) => { + return ( +
{ + if (el) { + scrollTargets.current[helpItem.label] = el; + } + }} + > + {helpItem.description} +
+ ); + })} +
+ ); + })} +
+
+ + ); +} + +export const DocumentationMainContent = React.memo(DocumentationContent); diff --git a/packages/kbn-language-documentation-popover/src/components/shared/documentation_navigation.tsx b/packages/kbn-language-documentation-popover/src/components/shared/documentation_navigation.tsx new file mode 100644 index 0000000000000..c8202d0ea448f --- /dev/null +++ b/packages/kbn-language-documentation-popover/src/components/shared/documentation_navigation.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import React from 'react'; +import { css } from '@emotion/react'; +import { + EuiFlexItem, + EuiFlexGroup, + EuiFormRow, + EuiLink, + EuiText, + useEuiTheme, + EuiFieldSearch, + EuiComboBox, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface DocumentationNavProps { + searchText: string; + setSearchText: (text: string) => void; + onNavigationChange: (selectedOptions: Array<{ label: string }>) => void; + filteredGroups?: Array<{ label: string }>; + linkToDocumentation?: string; + selectedSection?: string; +} + +function DocumentationNav({ + searchText, + setSearchText, + onNavigationChange, + filteredGroups, + linkToDocumentation, + selectedSection, +}: DocumentationNavProps) { + const { euiTheme } = useEuiTheme(); + + return ( + <> + + + + + {i18n.translate('languageDocumentationPopover.esqlDocsLinkLabel', { + defaultMessage: 'View full ES|QL documentation', + })} + + + ) + } + > + + + + + { + setSearchText(e.target.value); + }} + data-test-subj="language-documentation-navigation-search" + placeholder={i18n.translate('languageDocumentationPopover.searchPlaceholder', { + defaultMessage: 'Search', + })} + fullWidth + compressed + /> + + + + ); +} + +export const DocumentationNavigation = React.memo(DocumentationNav); diff --git a/packages/kbn-language-documentation-popover/src/components/shared/index.ts b/packages/kbn-language-documentation-popover/src/components/shared/index.ts new file mode 100644 index 0000000000000..f00df2358d39a --- /dev/null +++ b/packages/kbn-language-documentation-popover/src/components/shared/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { DocumentationNavigation } from './documentation_navigation'; +export { DocumentationMainContent } from './documentation_content'; diff --git a/packages/kbn-text-based-editor/src/inline_documentation/esql_documentation_sections.tsx b/packages/kbn-language-documentation-popover/src/sections/esql_documentation_sections.tsx similarity index 79% rename from packages/kbn-text-based-editor/src/inline_documentation/esql_documentation_sections.tsx rename to packages/kbn-language-documentation-popover/src/sections/esql_documentation_sections.tsx index d0c136cf6d01e..21e550f4ca5eb 100644 --- a/packages/kbn-text-based-editor/src/inline_documentation/esql_documentation_sections.tsx +++ b/packages/kbn-language-documentation-popover/src/sections/esql_documentation_sections.tsx @@ -17,11 +17,8 @@ const Markdown = (props: Parameters[0]) => ( export const initialSection = ( ); export const sourceCommands = { - label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.sourceCommands', { + label: i18n.translate('languageDocumentationPopover.documentationESQL.sourceCommands', { defaultMessage: 'Source commands', }), description: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.commandsDescription', + 'languageDocumentationPopover.documentationESQL.commandsDescription', { defaultMessage: `A source command produces a table, typically with data from Elasticsearch. ES|QL supports the following source commands.`, } ), items: [ { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.from', - { - defaultMessage: 'FROM', - } - ), + label: i18n.translate('languageDocumentationPopover.documentationESQL.from', { + defaultMessage: 'FROM', + }), description: ( \` source command returns information about the deployment and its capabilities: @@ -186,28 +173,25 @@ The \`SHOW \` source command returns information about the deployment and }; export const processingCommands = { - label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.processingCommands', { + label: i18n.translate('languageDocumentationPopover.documentationESQL.processingCommands', { defaultMessage: 'Processing commands', }), description: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.processingCommandsDescription', + 'languageDocumentationPopover.documentationESQL.processingCommandsDescription', { defaultMessage: `Processing commands change an input table by adding, removing, or changing rows and columns. ES|QL supports the following processing commands.`, } ), items: [ { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.dissect', - { - defaultMessage: 'DISSECT', - } - ), + label: i18n.translate('languageDocumentationPopover.documentationESQL.dissect', { + defaultMessage: 'DISSECT', + }), description: ( \` type conversion functions. +The \`::\` operator provides a convenient alternative syntax to the \`TO_\` type conversion functions. Example: \`\`\` @@ -943,16 +882,13 @@ ROW ver = CONCAT(("0"::INT + 1)::STRING, ".2.3")::VERSION ), }, { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.inOperator', - { - defaultMessage: 'IN', - } - ), + label: i18n.translate('languageDocumentationPopover.documentationESQL.inOperator', { + defaultMessage: 'IN', + }), description: ( { + const groups: Array<{ + label: string; + description?: string; + items: Array<{ label: string; description?: JSX.Element }>; + }> = []; + const { + sourceCommands, + processingCommands, + initialSection, + scalarFunctions, + aggregationFunctions, + groupingFunctions, + operators, + } = await import('./esql_documentation_sections'); + groups.push({ + label: i18n.translate('languageDocumentationPopover.esqlSections.initialSectionLabel', { + defaultMessage: 'ES|QL', + }), + items: [], + }); + groups.push( + sourceCommands, + processingCommands, + scalarFunctions, + aggregationFunctions, + groupingFunctions, + operators + ); + return { + groups, + initialSection, + }; +}; diff --git a/packages/kbn-language-documentation-popover/src/types.ts b/packages/kbn-language-documentation-popover/src/types.ts new file mode 100644 index 0000000000000..863c00b94e21e --- /dev/null +++ b/packages/kbn-language-documentation-popover/src/types.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export interface LanguageDocumentationSections { + groups: Array<{ + label: string; + description?: string; + items: Array<{ label: string; description?: JSX.Element }>; + }>; + initialSection: JSX.Element; +} diff --git a/packages/kbn-language-documentation-popover/src/utils/get_filtered_groups.test.tsx b/packages/kbn-language-documentation-popover/src/utils/get_filtered_groups.test.tsx new file mode 100644 index 0000000000000..9dfe443f10b0b --- /dev/null +++ b/packages/kbn-language-documentation-popover/src/utils/get_filtered_groups.test.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import React from 'react'; +import { Markdown } from '@kbn/shared-ux-markdown'; +import { getFilteredGroups } from './get_filtered_groups'; + +describe('getFilteredGroups', () => { + const sections = { + groups: [ + { + label: 'Section one', + description: 'Section 1 description', + items: [], + }, + { + label: 'Section two', + items: [ + { + label: 'Section two item 1 blah blah', + description: ( + + ), + }, + { + label: 'Section two item 2', + description: ( + + ), + }, + ], + }, + { + label: 'Section three ', + items: [ + { + label: 'Section three item 1', + description: ( + + ), + }, + { + label: 'Section three item 2', + description: ( + + ), + }, + ], + }, + ], + initialSection: Here is the initial section, + }; + test('Should return the sections as it gets them if the search string is empty', () => { + const filteredSections = getFilteredGroups('', false, sections); + expect(filteredSections).toStrictEqual([ + ...sections.groups.map((group) => ({ ...group, options: group.items })), + ]); + }); + + test('Should return the 2 last sections as it gets them if the search string is empty and the numOfGroupsToOmit is set to 1', () => { + const filteredSections = getFilteredGroups('', false, sections, 1); + expect(filteredSections).toStrictEqual([ + ...sections.groups.slice(1).map((group) => ({ ...group, options: group.items })), + ]); + }); + + test('Should return the section two as it gets it if the search string is asking for this', () => { + const filteredSections = getFilteredGroups('tWo', false, sections); + expect(filteredSections).toStrictEqual([ + { ...sections.groups[1], options: sections.groups[1].items }, + ]); + }); + + test('Should return the section two filtered on the search string if it is allowed to search in description', () => { + const filteredSections = getFilteredGroups('Section two item 1 blah blah', true, sections); + expect(filteredSections).toStrictEqual([ + { ...sections.groups[1], options: [sections.groups[1].items[0]] }, + ]); + }); +}); diff --git a/packages/kbn-language-documentation-popover/src/utils/get_filtered_groups.ts b/packages/kbn-language-documentation-popover/src/utils/get_filtered_groups.ts new file mode 100644 index 0000000000000..2ac252bdad775 --- /dev/null +++ b/packages/kbn-language-documentation-popover/src/utils/get_filtered_groups.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import type { LanguageDocumentationSections } from '../types'; +import { elementToString } from './element_to_string'; + +export const getFilteredGroups = ( + searchText: string, + searchInDescription?: boolean, + sections?: LanguageDocumentationSections, + numOfGroupsToOmit?: number +) => { + const normalizedSearchText = searchText.trim().toLocaleLowerCase(); + return sections?.groups + .slice(numOfGroupsToOmit ?? 0) + .map((group) => { + const options = group.items.filter((helpItem) => { + return ( + !normalizedSearchText || + helpItem.label.toLocaleLowerCase().includes(normalizedSearchText) || + // Converting the JSX element to a string first + (searchInDescription && + elementToString(helpItem.description) + ?.toLocaleLowerCase() + .includes(normalizedSearchText)) + ); + }); + return { ...group, options }; + }) + .filter((group) => { + if (group.options.length > 0 || !normalizedSearchText) { + return true; + } + return group.label.toLocaleLowerCase().includes(normalizedSearchText); + }); +}; diff --git a/packages/kbn-language-documentation-popover/tsconfig.json b/packages/kbn-language-documentation-popover/tsconfig.json index 48da6397a6448..f613b6cb759aa 100644 --- a/packages/kbn-language-documentation-popover/tsconfig.json +++ b/packages/kbn-language-documentation-popover/tsconfig.json @@ -5,6 +5,7 @@ "types": [ "jest", "node", + "@emotion/react/types/css-prop", ] }, "include": [ diff --git a/packages/kbn-text-based-editor/README.md b/packages/kbn-text-based-editor/README.md index 2bb9ae5887f24..ae7846a58d207 100644 --- a/packages/kbn-text-based-editor/README.md +++ b/packages/kbn-text-based-editor/README.md @@ -14,7 +14,6 @@ In order to enable text based languages on your unified search bar add `textBase ## Languages supported -- SQL: based on the Elasticsearch sql api - ESQL: based on the Elastisearch esql api diff --git a/packages/kbn-text-based-editor/package.json b/packages/kbn-text-based-editor/package.json index 3eeb282f953c2..47d3d426b21f2 100644 --- a/packages/kbn-text-based-editor/package.json +++ b/packages/kbn-text-based-editor/package.json @@ -5,10 +5,5 @@ "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0", "sideEffects": [ "*.scss" - ], - "scripts": { - "make:docs": "ts-node --transpileOnly scripts/generate_esql_docs.ts", - "postmake:docs": "yarn run lint:fix", - "lint:fix": "cd ../.. && node ./scripts/eslint --fix ./packages/kbn-text-based-editor/src/inline_documentation/generated" - } + ] } diff --git a/packages/kbn-text-based-editor/src/editor_footer/index.tsx b/packages/kbn-text-based-editor/src/editor_footer/index.tsx index fa60eb1a32f44..6468a2c08a8bc 100644 --- a/packages/kbn-text-based-editor/src/editor_footer/index.tsx +++ b/packages/kbn-text-based-editor/src/editor_footer/index.tsx @@ -7,18 +7,25 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { memo, useState, useCallback, useEffect, useMemo } from 'react'; +import React, { memo, useState, useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiText, EuiFlexGroup, EuiFlexItem, EuiCode } from '@elastic/eui'; +import { + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiCode, + EuiButtonIcon, + EuiButtonEmpty, +} from '@elastic/eui'; import { Interpolation, Theme, css } from '@emotion/react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { - LanguageDocumentationPopover, - type LanguageDocumentationSections, + LanguageDocumentationInline, + LanguageDocumentationFlyout, } from '@kbn/language-documentation-popover'; import { getLimitFromESQLQuery } from '@kbn/esql-utils'; -import { type MonacoMessage, getDocumentationSections } from '../helpers'; +import { type MonacoMessage } from '../helpers'; import { ErrorsWarningsFooterPopover } from './errors_warnings_popover'; import { QueryHistoryAction, QueryHistory } from './query_history'; import { SubmitFeedbackComponent } from './feedback_component'; @@ -43,8 +50,6 @@ interface EditorFooterProps { updateQuery: (qs: string) => void; isHistoryOpen: boolean; setIsHistoryOpen: (status: boolean) => void; - isHelpMenuOpen: boolean; - setIsHelpMenuOpen: (status: boolean) => void; measuredContainerWidth: number; hideRunQueryText?: boolean; editorIsInline?: boolean; @@ -52,6 +57,7 @@ interface EditorFooterProps { hideTimeFilterInfo?: boolean; hideQueryHistory?: boolean; isInCompactMode?: boolean; + displayDocumentationAsFlyout?: boolean; } export const EditorFooter = memo(function EditorFooter({ @@ -71,18 +77,15 @@ export const EditorFooter = memo(function EditorFooter({ setIsHistoryOpen, hideQueryHistory, isInCompactMode, + displayDocumentationAsFlyout, measuredContainerWidth, code, - isHelpMenuOpen, - setIsHelpMenuOpen, }: EditorFooterProps) { const kibana = useKibana(); const { docLinks } = kibana.services; - const [isErrorPopoverOpen, setIsErrorPopoverOpen] = useState(false); + const [isLanguageComponentOpen, setIsLanguageComponentOpen] = useState(false); const [isWarningPopoverOpen, setIsWarningPopoverOpen] = useState(false); - const [documentationSections, setDocumentationSections] = - useState(); const onUpdateAndSubmit = useCallback( (qs: string) => { @@ -98,17 +101,17 @@ export const EditorFooter = memo(function EditorFooter({ [runQuery, updateQuery] ); - const limit = useMemo(() => getLimitFromESQLQuery(code), [code]); + const toggleHistoryComponent = useCallback(() => { + setIsHistoryOpen(!isHistoryOpen); + setIsLanguageComponentOpen(false); + }, [isHistoryOpen, setIsHistoryOpen]); - useEffect(() => { - async function getDocumentation() { - const sections = await getDocumentationSections('esql'); - setDocumentationSections(sections); - } - if (!documentationSections) { - getDocumentation(); - } - }, [documentationSections]); + const toggleLanguageComponent = useCallback(async () => { + setIsLanguageComponentOpen(!isLanguageComponentOpen); + setIsHistoryOpen(false); + }, [isLanguageComponentOpen, setIsHistoryOpen]); + + const limit = useMemo(() => getLimitFromESQLQuery(code), [code]); return (
)} - {documentationSections && !editorIsInline && ( - - + toggleLanguageComponent()} + css={css` + cursor: pointer; + `} + /> + - + )} @@ -318,34 +318,14 @@ export const EditorFooter = memo(function EditorFooter({ {!hideQueryHistory && ( setIsHistoryOpen(!isHistoryOpen)} + toggleHistory={toggleHistoryComponent} isHistoryOpen={isHistoryOpen} isSpaceReduced={true} /> )} - {documentationSections && ( - - - - )} + + + @@ -362,6 +342,11 @@ export const EditorFooter = memo(function EditorFooter({ /> )} + {isLanguageComponentOpen && editorIsInline && ( + + + + )} ); }); diff --git a/packages/kbn-text-based-editor/src/helpers.ts b/packages/kbn-text-based-editor/src/helpers.ts index 0ba6d5004606f..b1bb3aaf826a7 100644 --- a/packages/kbn-text-based-editor/src/helpers.ts +++ b/packages/kbn-text-based-editor/src/helpers.ts @@ -161,43 +161,6 @@ export const parseErrors = (errors: Error[], code: string): MonacoMessage[] => { }); }; -export const getDocumentationSections = async (language: string) => { - const groups: Array<{ - label: string; - description?: string; - items: Array<{ label: string; description?: JSX.Element }>; - }> = []; - if (language === 'esql') { - const { - sourceCommands, - processingCommands, - initialSection, - scalarFunctions, - aggregationFunctions, - groupingFunctions, - operators, - } = await import('./inline_documentation/esql_documentation_sections'); - groups.push({ - label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.esql', { - defaultMessage: 'ES|QL', - }), - items: [], - }); - groups.push( - sourceCommands, - processingCommands, - scalarFunctions, - aggregationFunctions, - groupingFunctions, - operators - ); - return { - groups, - initialSection, - }; - } -}; - export const getIndicesList = async (dataViews: DataViewsPublicPluginStart) => { const indices = await dataViews.getIndices({ showAllIndices: false, diff --git a/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx b/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx index d8fe9512691a1..0855d0326263f 100644 --- a/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx +++ b/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx @@ -16,21 +16,6 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { TextBasedLanguagesEditor } from './text_based_languages_editor'; import type { TextBasedLanguagesEditorProps } from './types'; import { ReactWrapper } from 'enzyme'; - -jest.mock('./helpers', () => { - const module = jest.requireActual('./helpers'); - return { - ...module, - getDocumentationSections: () => ({ - groups: [ - { - label: 'How it works', - items: [], - }, - ], - }), - }; -}); import { of } from 'rxjs'; describe('TextBasedLanguagesEditor', () => { @@ -133,9 +118,6 @@ describe('TextBasedLanguagesEditor', () => { expect( component!.find('[data-test-subj="TextBasedLangEditor-toggleWordWrap"]').length ).not.toBe(0); - expect(component!.find('[data-test-subj="TextBasedLangEditor-documentation"]').length).not.toBe( - 0 - ); }); it('should render the resize for the expanded code editor mode', async () => { @@ -156,6 +138,18 @@ describe('TextBasedLanguagesEditor', () => { expect(component.find('[data-test-subj="TextBasedLangEditor-run-query"]').length).not.toBe(0); }); + it('should render the doc icon if the displayDocumentationAsFlyout is true', async () => { + const newProps = { + ...props, + displayDocumentationAsFlyout: true, + editorIsInline: false, + }; + const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps })); + expect(component.find('[data-test-subj="TextBasedLangEditor-documentation"]').length).not.toBe( + 0 + ); + }); + it('should not render the run query text if the hideRunQueryText prop is set to true', async () => { const newProps = { ...props, diff --git a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx index d6936bde6fcbe..2ebd5a42a966a 100644 --- a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx +++ b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx @@ -78,6 +78,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ hideTimeFilterInfo, hideQueryHistory, hasOutline, + displayDocumentationAsFlyout, }: TextBasedLanguagesEditorProps) { const popoverRef = useRef(null); const datePickerOpenStatusRef = useRef(false); @@ -108,7 +109,6 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ const [isHistoryOpen, setIsHistoryOpen] = useState(false); const [isCodeEditorExpandedFocused, setIsCodeEditorExpandedFocused] = useState(false); - const [isLanguagePopoverOpen, setIsLanguagePopoverOpen] = useState(false); const [isQueryLoading, setIsQueryLoading] = useState(true); const [abortController, setAbortController] = useState(new AbortController()); @@ -738,8 +738,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ setIsHistoryOpen={toggleHistory} measuredContainerWidth={measuredEditorWidth} hideQueryHistory={hideHistoryComponent} - isHelpMenuOpen={isLanguagePopoverOpen} - setIsHelpMenuOpen={setIsLanguagePopoverOpen} + displayDocumentationAsFlyout={displayDocumentationAsFlyout} /> { renderESQLPopover(); expect(screen.getByTestId('esql-menu-button')).toBeInTheDocument(); await userEvent.click(screen.getByRole('button')); - + expect(screen.getByTestId('esql-quick-reference')).toBeInTheDocument(); expect(screen.getByTestId('esql-examples')).toBeInTheDocument(); expect(screen.getByTestId('esql-about')).toBeInTheDocument(); expect(screen.getByTestId('esql-feedback')).toBeInTheDocument(); diff --git a/src/plugins/unified_search/public/query_string_input/esql_menu_popover.tsx b/src/plugins/unified_search/public/query_string_input/esql_menu_popover.tsx index 003827344c307..d684448670c42 100644 --- a/src/plugins/unified_search/public/query_string_input/esql_menu_popover.tsx +++ b/src/plugins/unified_search/public/query_string_input/esql_menu_popover.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useCallback } from 'react'; import { EuiPopover, EuiButton, @@ -19,6 +19,7 @@ import { import { useKibana } from '@kbn/kibana-react-plugin/public'; import { i18n } from '@kbn/i18n'; import { FEEDBACK_LINK } from '@kbn/esql-utils'; +import { LanguageDocumentationFlyout } from '@kbn/language-documentation-popover'; import type { IUnifiedSearchPluginServices } from '../types'; export const ESQLMenuPopover = () => { @@ -26,15 +27,33 @@ export const ESQLMenuPopover = () => { const { docLinks } = kibana.services; const [isESQLMenuPopoverOpen, setIsESQLMenuPopoverOpen] = useState(false); + const [isLanguageComponentOpen, setIsLanguageComponentOpen] = useState(false); + + const toggleLanguageComponent = useCallback(async () => { + setIsLanguageComponentOpen(!isLanguageComponentOpen); + setIsESQLMenuPopoverOpen(false); + }, [isLanguageComponentOpen]); + const esqlPanelItems = useMemo(() => { const panelItems: EuiContextMenuPanelProps['items'] = []; panelItems.push( + toggleLanguageComponent()} + > + {i18n.translate('unifiedSearch.query.queryBar.esqlMenu.quickReference', { + defaultMessage: 'Quick Reference', + })} + , setIsESQLMenuPopoverOpen(false)} > {i18n.translate('unifiedSearch.query.queryBar.esqlMenu.documentation', { defaultMessage: 'Documentation', @@ -46,6 +65,7 @@ export const ESQLMenuPopover = () => { data-test-subj="esql-examples" target="_blank" href={docLinks.links.query.queryESQLExamples} + onClick={() => setIsESQLMenuPopoverOpen(false)} > {i18n.translate('unifiedSearch.query.queryBar.esqlMenu.documentation', { defaultMessage: 'Example queries', @@ -58,6 +78,7 @@ export const ESQLMenuPopover = () => { data-test-subj="esql-feedback" target="_blank" href={FEEDBACK_LINK} + onClick={() => setIsESQLMenuPopoverOpen(false)} > {i18n.translate('unifiedSearch.query.queryBar.esqlMenu.documentation', { defaultMessage: 'Submit feedback', @@ -65,32 +86,44 @@ export const ESQLMenuPopover = () => { ); return panelItems; - }, [docLinks.links.query.queryESQL, docLinks.links.query.queryESQLExamples]); + }, [ + docLinks.links.query.queryESQL, + docLinks.links.query.queryESQLExamples, + toggleLanguageComponent, + ]); return ( - setIsESQLMenuPopoverOpen(!isESQLMenuPopoverOpen)} - data-test-subj="esql-menu-button" - size="s" - > - {i18n.translate('unifiedSearch.query.queryBar.esqlMenu.label', { - defaultMessage: 'ES|QL help', - })} - - } - panelProps={{ - ['data-test-subj']: 'esql-menu-popover', - css: { width: 240 }, - }} - isOpen={isESQLMenuPopoverOpen} - closePopover={() => setIsESQLMenuPopoverOpen(false)} - panelPaddingSize="s" - display="block" - > - - + <> + setIsESQLMenuPopoverOpen(!isESQLMenuPopoverOpen)} + data-test-subj="esql-menu-button" + size="s" + > + {i18n.translate('unifiedSearch.query.queryBar.esqlMenu.label', { + defaultMessage: 'ES|QL help', + })} + + } + panelProps={{ + ['data-test-subj']: 'esql-menu-popover', + css: { width: 240 }, + }} + isOpen={isESQLMenuPopoverOpen} + closePopover={() => setIsESQLMenuPopoverOpen(false)} + panelPaddingSize="s" + display="block" + > + + + + ); }; diff --git a/src/plugins/unified_search/tsconfig.json b/src/plugins/unified_search/tsconfig.json index 909c0031b5a31..e836f3c6daa67 100644 --- a/src/plugins/unified_search/tsconfig.json +++ b/src/plugins/unified_search/tsconfig.json @@ -46,7 +46,8 @@ "@kbn/data-view-utils", "@kbn/esql-utils", "@kbn/react-kibana-mount", - "@kbn/field-utils" + "@kbn/field-utils", + "@kbn/language-documentation-popover" ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx index 6671e4c73c2e8..8e3d48c47c789 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx @@ -266,6 +266,7 @@ export const IndexDataVisualizerESQL: FC = (dataVi detectedTimestamp={currentDataView?.timeFieldName} hideRunQueryText={false} isLoading={queryHistoryStatus ?? false} + displayDocumentationAsFlyout /> diff --git a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.tsx b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.tsx index c8ce47ce68ac4..da8559a958d99 100644 --- a/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/rule_types/es_query/expression/esql_query_expression.tsx @@ -198,6 +198,7 @@ export const EsqlQueryExpression: React.FC< detectedTimestamp={detectedTimestamp} hideRunQueryText={true} isLoading={isLoading} + editorIsInline hasOutline /> diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 942c117344f5d..36ae1159d9ab6 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -7084,255 +7084,253 @@ "telemetry.usageCollectionConstant": "collecte de données d’utilisation", "telemetry.usageDataTitle": "Collecte de données d’utilisation", "textBasedEditor.query.textBasedLanguagesEditor.aborted": "La demande a été annulée", - "textBasedEditor.query.textBasedLanguagesEditor.aggregationFunctions": "Fonctions d'agrégation", - "textBasedEditor.query.textBasedLanguagesEditor.aggregationFunctionsDocumentationESQLDescription": "Ces fonctions peuvent être utilisées avec STATS...BY :", + "languageDocumentationPopover.documentationESQL.aggregationFunctions": "Fonctions d'agrégation", + "languageDocumentationPopover.documentationESQL.aggregationFunctionsDocumentationESQLDescription": "Ces fonctions peuvent être utilisées avec STATS...BY :", "textBasedEditor.query.textBasedLanguagesEditor.cancel": "Annuler", "textBasedEditor.query.textBasedLanguagesEditor.collapseLabel": "Réduire", - "textBasedEditor.query.textBasedLanguagesEditor.commandsDescription": "Une commande source produit un tableau, habituellement avec des données issues d'Elasticsearch. ES|QL est compatible avec les commandes sources suivantes.", + "languageDocumentationPopover.documentationESQL.commandsDescription": "Une commande source produit un tableau, habituellement avec des données issues d'Elasticsearch. ES|QL est compatible avec les commandes sources suivantes.", "textBasedEditor.query.textBasedLanguagesEditor.disableWordWrapLabel": "Supprimer les sauts de ligne des barres verticales", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.abs": "ABS", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.abs.markdown": "\n\n ### ABS\n Renvoie la valeur absolue.\n\n ````\n Numéro ROW = -1.0 \n | EVAL abs_number = ABS(number)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.acos": "ACOS", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.acos.markdown": "\n\n ### ACOS\n Renvoie l'arc cosinus de `n` sous forme d'angle, exprimé en radians.\n\n ````\n ROW a=.9\n | EVAL acos=ACOS(a)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.asin": "ASIN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.asin.markdown": "\n\n ### ASIN\n Renvoie l'arc sinus de l'entrée\n expression numérique sous forme d'angle, exprimée en radians.\n\n ````\n ROW a=.9\n | EVAL asin=ASIN(a)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.atan": "ATAN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.atan.markdown": "\n\n ### ATAN\n Renvoie l'arc tangente de l'entrée\n expression numérique sous forme d'angle, exprimée en radians.\n\n ````\n ROW a=.12.9\n | EVAL atan=ATAN(a)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.atan2": "ATAN2", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.atan2.markdown": "\n\n ### ATAN2\n L'angle entre l'axe positif des x et le rayon allant de\n l'origine au point (x , y) dans le plan cartésien, exprimée en radians.\n\n ````\n ROW y=12.9, x=.6\n | EVAL atan2=ATAN2(y, x)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.autoBucketFunction": "COMPARTIMENT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.autoBucketFunction.markdown": "### COMPARTIMENT\nCréer des groupes de valeurs, des compartiments (\"buckets\"), à partir d'une entrée d'un numéro ou d'un horodatage. La taille des compartiments peut être fournie directement ou choisie selon une plage de valeurs et de décompte recommandée.\n\n`BUCKET` a deux modes de fonctionnement : \n\n1. Dans lequel la taille du compartiment est calculée selon la recommandation de décompte d'un compartiment (quatre paramètres) et une plage.\n2. Dans lequel la taille du compartiment est fournie directement (deux paramètres).\n\nAvec un nombre cible de compartiments, le début d'une plage et la fin d'une plage, `BUCKET` choisit une taille de compartiment appropriée afin de générer le nombre cible de compartiments ou moins.\n\nPar exemple, demander jusqu'à 20 compartiments pour une année organisera les données en intervalles mensuels :\n\n````\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hire_date = MV_SORT(VALUES(hire_date)) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT hire_date\n````\n\n**REMARQUE** : Le but n'est pas de fournir le nombre précis de compartiments, mais plutôt de sélectionner une plage qui fournit, tout au plus, le nombre cible de compartiments.\n\nVous pouvez combiner `BUCKET` avec une agrégation pour créer un histogramme :\n\n````\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_month = COUNT(*) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT month\n````\n\n**REMARQUE** : `BUCKET` ne crée pas de compartiments qui ne correspondent à aucun document. C'est pourquoi, dans l'exemple précédent, il manque 1985-03-01 ainsi que d'autres dates.\n\nDemander d'autres compartiments peut résulter en une plage réduite. Par exemple, demander jusqu'à 100 compartiments en un an résulte en des compartiments hebdomadaires :\n\n````\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_week = COUNT(*) BY week = BUCKET(hire_date, 100, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT week\n````\n\n**REMARQUE** : `AUTO_BUCKET` ne filtre aucune ligne. Il n'utilise que la plage fournie pour choisir une taille de compartiment appropriée. Pour les lignes dont la valeur se situe en dehors de la plage, il renvoie une valeur de compartiment qui correspond à un compartiment situé en dehors de la plage. Associez `BUCKET` à `WHERE` pour filtrer les lignes.\n\nSi la taille de compartiment désirée est connue à l'avance, fournissez-la comme second argument, en ignorant la plage :\n\n````\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_week = COUNT(*) BY week = BUCKET(hire_date, 1 week)\n| SORT week\n````\n\n**REMARQUE** : Lorsque vous fournissez la taille du compartiment comme second argument, ce dernier doit être une période temporelle ou une durée.\n\n`BUCKET` peut également être utilisé pour des champs numériques. Par exemple, pour créer un histogramme de salaire :\n\n````\nFROM employees\n| STATS COUNT(*) by bs = BUCKET(salary, 20, 25324, 74999)\n| SORT bs\n````\n\nContrairement à l'exemple précédent qui filtre intentionnellement sur une plage temporelle, vous n'avez pas souvent besoin de filtrer sur une plage numérique. Vous devez trouver les valeurs min et max séparément. ES|QL n'a pas encore de façon aisée d'effectuer cette opération automatiquement.\n\nLa plage peut être ignorée si la taille désirée de compartiment est connue à l'avance. Fournissez-la simplement comme second argument :\n\n````\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS c = COUNT(1) BY b = BUCKET(salary, 5000.)\n| SORT b\n````\n\n**REMARQUE** : Lorsque vous fournissez la taille du compartiment comme second argument, elle doit être de type à **virgule flottante**.\n\nVoici un exemple sur comment créer des compartiments horaires pour les dernières 24 heures, et calculer le nombre d'événements par heure :\n\n````\nFROM sample_data\n| WHERE @timestamp >= NOW() - 1 day and @timestamp < NOW()\n| STATS COUNT(*) BY bucket = BUCKET(@timestamp, 25, NOW() - 1 day, NOW())\n````\n\nVoici un exemple permettant de créer des compartiments mensuels pour l'année 1985, et calculer le salaire moyen par mois d'embauche :\n\n````\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS AVG(salary) BY bucket = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT bucket\n````\n\n`BUCKET` peut être utilisé pour les parties de groupage et d'agrégation de la commande `STATS …​ BY ...`, tant que la partie d'agrégation de la fonction est **référencée par un alias défini dans la partie de groupage**, ou que celle-ci est invoquée avec exactement la même expression.\n\nPar exemple :\n\n````\nFROM employees\n| STATS s1 = b1 + 1, s2 = BUCKET(salary / 1000 + 999, 50.) + 2 BY b1 = BUCKET(salary / 100 + 99, 50.), b2 = BUCKET(salary / 1000 + 999, 50.)\n| SORT b1, b2\n| KEEP s1, b1, s2, b2\n````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.binaryOperators": "Opérateurs binaires", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.binaryOperators.markdown": "### Opérateurs binaires\nLes opérateurs de comparaison binaire suivants sont pris en charge :\n\n* égalité : `==`\n* inégalité : `!=`\n* inférieur à : `<`\n* inférieur ou égal à : `<=`\n* supérieur à : `>`\n* supérieur ou égal à : `>=`\n* ajouter : `+`\n* soustraire : `-`\n* multiplier par : `*`\n* diviser par : `/`\n* module : `%`\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.booleanOperators": "Opérateurs booléens", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.booleanOperators.markdown": "### Opérateurs booléens\nLes opérateurs booléens suivants sont pris en charge :\n\n* `AND`\n* `OR`\n* `NOT`\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.bucket": "COMPARTIMENT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.bucket.markdown": "\n\n ### COMPARTIMENT\n Créer des groupes de valeurs, des compartiments (\"buckets\"), à partir d'une entrée d'un numéro ou d'un horodatage.\n La taille des compartiments peut être fournie directement ou choisie selon une plage de valeurs et de décompte recommandée.\n\n ````\n FROM employees\n | WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n | STATS hire_date = MV_SORT(VALUES(hire_date)) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n | SORT hire_date\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.case": "CASE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.case.markdown": "\n\n ### CAS\n Accepte les paires de conditions et de valeurs. La fonction renvoie la valeur qui\n appartient à la première condition étant évaluée comme `true`.\n\n Si le nombre d'arguments est impair, le dernier argument est la valeur par défaut qui est\n renvoyée si aucune condition ne correspond. Si le nombre d'arguments est pair, et\n qu'aucune condition ne correspond, la fonction renvoie `null`.\n\n ````\n FROM employees\n | EVAL type = CASE(\n languages <= 1, \"monolingual\",\n languages <= 2, \"bilingual\",\n \"polyglot\")\n | KEEP emp_no, languages, type\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.castOperator": "Cast (::)", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.castOperator.markdown": "### CAST (`::`)\nL'opérateur `::` fournit une syntaxe alternative pratique au type de converstion de fonction `TO_`.\n\nExemple :\n````\nROW ver = CONCAT((\"0\"::INT + 1)::STRING, \".2.3\")::VERSION\n````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cbrt": "CBRT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cbrt.markdown": "\n\n ### CBRT\n Renvoie la racine cubique d'un nombre. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée.\n La racine cubique de l’infini est nulle.\n\n ````\n ROW d = 1000.0\n | EVAL c = cbrt(d)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ceil": "CEIL", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ceil.markdown": "\n\n ### CEIL\n Arrondir un nombre à l'entier supérieur.\n\n ```\n ROW a=1.8\n | EVAL a=CEIL(a)\n ```\n Remarque : Il s'agit d'un noop pour `long` (y compris non signé) et `integer`. Pour `double`, la fonction choisit la valeur `double` la plus proche de l'entier, de manière similaire à la méthode Math.ceil.\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cidr_match": "CIDR_MATCH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cidr_match.markdown": "\n\n ### CIDR_MATCH\n Renvoie `true` si l'IP fournie est contenue dans l'un des blocs CIDR fournis.\n\n ````\n FROM hosts \n | WHERE CIDR_MATCH(ip1, \"127.0.0.2/32\", \"127.0.0.3/32\") \n | KEEP card, host, ip0, ip1\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.coalesce": "COALESCE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.coalesce.markdown": "\n\n ### COALESCE\n Renvoie le premier de ses arguments qui n'est pas nul. Si tous les arguments sont nuls, `null` est renvoyé.\n\n ````\n ROW a=null, b=\"b\"\n | EVAL COALESCE(a, b)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.concat": "CONCAT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.concat.markdown": "\n\n ### CONCAT\n Concatène deux ou plusieurs chaînes.\n\n ```\n FROM employees\n | KEEP first_name, last_name\n | EVAL fullname = CONCAT(first_name, \" \", last_name)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cos": "COS", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cos.markdown": "\n\n ### COS\n Renvoie le cosinus d'un angle.\n\n ````\n ROW a=1.8 \n | EVAL cos=COS(a)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cosh": "COSH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cosh.markdown": "\n\n ### COSH\n Renvoie le cosinus hyperbolique d'un angle.\n\n ````\n ROW a=1.8 \n | EVAL cosh=COSH(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_diff": "DATE_DIFF", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_diff.markdown": "\n\n ### DATE_DIFF\n Soustrait le `startTimestamp` du `endTimestamp` et renvoie la différence en multiples `d'unité`.\n Si `startTimestamp` est postérieur à `endTimestamp`, des valeurs négatives sont renvoyées.\n\n ````\n ROW date1 = TO_DATETIME(\"2023-12-02T11:00:00.000Z\"), date2 = TO_DATETIME(\"2023-12-02T11:00:00.001Z\")\n | EVAL dd_ms = DATE_DIFF(\"microseconds\", date1, date2)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_extract": "DATE_EXTRACT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_extract.markdown": "\n\n ### DATE_EXTRACT\n Extrait des parties d'une date, telles que l'année, le mois, le jour, l'heure.\n\n ````\n ROW date = DATE_PARSE(\"yyyy-MM-dd\", \"2022-05-06\")\n | EVAL year = DATE_EXTRACT(\"year\", date)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_format": "DATE_FORMAT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_format.markdown": "\n\n ### DATE_FORMAT\n Renvoie une représentation sous forme de chaîne d'une date dans le format fourni.\n\n ````\n FROM employees\n | KEEP first_name, last_name, hire_date\n | EVAL hired = DATE_FORMAT(\"YYYY-MM-dd\", hire_date)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_parse": "DATE_PARSE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_parse.markdown": "\n\n ### DATE_PARSE\n Renvoie une date en analysant le deuxième argument selon le format spécifié dans le premier argument.\n\n ````\n ROW date_string = \"2022-05-06\"\n | EVAL date = DATE_PARSE(\"yyyy-MM-dd\", date_string)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_trunc": "DATE_TRUNC", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_trunc.markdown": "\n\n ### DATE_TRUNC\n Arrondit une date à l'intervalle le plus proche.\n\n ```\n FROM employees\n | KEEP first_name, last_name, hire_date\n | EVAL year_hired = DATE_TRUNC(1 year, hire_date)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.dissect": "DISSECT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.dissect.markdown": "### DISSECT\n`DISSECT` vous permet d'extraire des données structurées d'une chaîne. `DISSECT` compare la chaîne à un modèle basé sur les délimiteurs, et extrait les clés indiquées en tant que colonnes.\n\nPour obtenir la syntaxe des modèles \"dissect\", consultez [la documentation relative au processeur \"dissect\"](https://www.elastic.co/guide/en/elasticsearch/reference/current/dissect-processor.html).\n\n```\nROW a = \"1953-01-23T12:15:00Z - some text - 127.0.0.1\"\n| DISSECT a \"%'{Y}-%{M}-%{D}T%{h}:%{m}:%{s}Z - %{msg} - %{ip}'\"\n```` ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.drop": "DROP", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.drop.markdown": "### DROP\nAfin de supprimer certaines colonnes d'un tableau, utilisez `DROP` :\n \n```\nFROM employees\n| DROP height\n```\n\nPlutôt que de spécifier chaque colonne par son nom, vous pouvez utiliser des caractères génériques pour supprimer toutes les colonnes dont le nom correspond à un modèle :\n\n```\nFROM employees\n| DROP height*\n````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.e": "E", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.e.markdown": "\n\n ### E\n Retourne le nombre d'Euler.\n\n ````\n ROW E()\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ends_with": "ENDS_WITH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ends_with.markdown": "\n\n ### ENDS_WITH\n Renvoie une valeur booléenne qui indique si une chaîne de mots-clés se termine par une autre chaîne.\n\n ````\n FROM employees\n | KEEP last_name\n | EVAL ln_E = ENDS_WITH(last_name, \"d\")\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.enrich": "ENRICH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.enrich.markdown": "### ENRICH\nVous pouvez utiliser `ENRICH` pour ajouter les données de vos index existants aux enregistrements entrants. Une fonction similaire à l'[enrichissement par ingestion](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html), mais qui fonctionne au moment de la requête.\n\n```\nROW language_code = \"1\"\n| ENRICH languages_policy\n```\n\n`ENRICH` requiert l'exécution d'une [politique d'enrichissement](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html#enrich-policy). La politique d'enrichissement définit un champ de correspondance (un champ clé) et un ensemble de champs d'enrichissement.\n\n`ENRICH` recherche les enregistrements dans l'[index d'enrichissement](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html#enrich-index) en se basant sur la valeur du champ de correspondance. La clé de correspondance dans l'ensemble de données d'entrée peut être définie en utilisant `ON `. Si elle n'est pas spécifiée, la correspondance sera effectuée sur un champ portant le même nom que le champ de correspondance défini dans la politique d'enrichissement.\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a\n```\n\nVous pouvez indiquer quels attributs (parmi ceux définis comme champs d'enrichissement dans la politique) doivent être ajoutés au résultat, en utilisant la syntaxe `WITH , ...`.\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a WITH language_name\n```\n\nLes attributs peuvent également être renommés à l'aide de la syntaxe `WITH new_name=`\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a WITH name = language_name\n````\n\nPar défaut (si aucun `WITH` n'est défini), `ENRICH` ajoute au résultat tous les champs d'enrichissement définis dans la politique d'enrichissement.\n\nEn cas de collision de noms, les champs nouvellement créés remplacent les champs existants.\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.eval": "EVAL", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.eval.markdown": "### EVAL\n`EVAL` permet d'ajouter des colonnes :\n\n````\nFROM employees\n| KEEP first_name, last_name, height\n| EVAL height_feet = height * 3.281, height_cm = height * 100\n````\n\nSi la colonne indiquée existe déjà, la colonne existante sera supprimée et la nouvelle colonne sera ajoutée au tableau :\n\n````\nFROM employees\n| KEEP first_name, last_name, height\n| EVAL height = height * 3.281\n````\n\n#### Fonctions\n`EVAL` prend en charge diverses fonctions de calcul des valeurs. Pour en savoir plus, consultez les fonctions.\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.floor": "FLOOR", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.floor.markdown": "\n\n ### FLOOR\n Arrondir un nombre à l'entier inférieur.\n\n ````\n ROW a=1.8\n | EVAL a=FLOOR(a)\n ````\n Remarque : Il s'agit d'un noop pour `long` (y compris non signé) et `integer`.\n Pour `double`, la fonction choisit la valeur `double` la plus proche de l'entier,\n de manière similaire à Math.floor.\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.from": "FROM", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.from_base64": "FROM_BASE64", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.from_base64.markdown": "\n\n ### FROM_BASE64\n Décodez une chaîne base64.\n\n ````\n row a = \"ZWxhc3RpYw==\" \n | eval d = from_base64(a)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.from.markdown": "### FROM\nLa commande source `FROM` renvoie un tableau contenant jusqu'à 10 000 documents issus d'un flux de données, d'un index ou d'un alias. Chaque ligne du tableau obtenu correspond à un document. Chaque colonne correspond à un champ et est accessible par le nom de ce champ.\n\n````\nFROM employees\n````\n\nVous pouvez utiliser des [calculs impliquant des dates](https://www.elastic.co/guide/en/elasticsearch/reference/current/api-conventions.html#api-date-math-index-names) pour désigner les indices, les alias et les flux de données. Cela peut s'avérer utile pour les données temporelles.\n\nUtilisez des listes séparées par des virgules ou des caractères génériques pour rechercher plusieurs flux de données, indices ou alias :\n\n````\nFROM employees-00001,employees-*\n````\n\n#### Métadonnées\n\nES|QL peut accéder aux champs de métadonnées suivants :\n\n* `_index` : l'index auquel appartient le document. Le champ est du type `keyword`.\n* `_id` : l'identifiant du document source. Le champ est du type `keyword`.\n* `_id` : la version du document source. Le champ est du type `long`.\n\nUtilisez la directive `METADATA` pour activer les champs de métadonnées :\n\n````\nFROM index [METADATA _index, _id]\n````\n\nLes champs de métadonnées ne sont disponibles que si la source des données est un index. Par conséquent, `FROM` est la seule commande source qui prend en charge la directive `METADATA`.\n\nUne fois activés, les champs sont disponibles pour les commandes de traitement suivantes, tout comme les autres champs de l'index :\n\n````\nFROM ul_logs, apps [METADATA _index, _version]\n| WHERE id IN (13, 14) AND _version == 1\n| EVAL key = CONCAT(_index, \"_\", TO_STR(id))\n| SORT id, _index\n| KEEP id, _index, _version, key\n````\n\nDe même, comme pour les champs d'index, une fois l'agrégation effectuée, un champ de métadonnées ne sera plus accessible aux commandes suivantes, sauf s'il est utilisé comme champ de regroupement :\n\n````\nFROM employees [METADATA _index, _id]\n| STATS max = MAX(emp_no) BY _index\n````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.greatest": "GREATEST", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.greatest.markdown": "\n\n ### GREATEST\n Renvoie la valeur maximale de plusieurs colonnes. Similaire à `MV_MAX`\n sauf que ceci est destiné à une exécution sur plusieurs colonnes à la fois.\n\n ````\n ROW a = 10, b = 20\n | EVAL g = GREATEST(a, b)\n ````\n Remarque : Lorsque cette fonction est exécutée sur les champs `keyword` ou `text`, elle renvoie la dernière chaîne dans l'ordre alphabétique. Lorsqu'elle est exécutée sur des colonnes `boolean`, elle renvoie `true` si l'une des valeurs l'est.\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.grok": "GROK", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.grok.markdown": "### GROK\n`GROK` vous permet d'extraire des données structurées d'une chaîne. `GROK` compare la chaîne à des modèles, sur la base d’expressions régulières, et extrait les modèles indiqués en tant que colonnes.\n\nPour obtenir la syntaxe des modèles \"grok\", consultez [la documentation relative au processeur \"grok\"](https://www.elastic.co/guide/en/elasticsearch/reference/current/grok-processor.html).\n\n````\nROW a = \"12 15.5 15.6 true\"\n| GROK a \"%'{NUMBER:b:int}' %'{NUMBER:c:float}' %'{NUMBER:d:double}' %'{WORD:e:boolean}'\"\n````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.inOperator": "IN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.inOperator.markdown": "### IN\nL'opérateur `IN` permet de tester si un champ ou une expression est égal à un élément d'une liste de littéraux, de champs ou d'expressions :\n\n````\nROW a = 1, b = 4, c = 3\n| WHERE c-a IN (3, b / 2, a)\n````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ip_prefix": "IP_PREFIX", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ip_prefix.markdown": "\n\n ### IP_PREFIX\n Tronque une adresse IP à une longueur de préfixe donnée.\n\n ````\n row ip4 = to_ip(\"1.2.3.4\"), ip6 = to_ip(\"fe80::cae2:65ff:fece:feb9\")\n | eval ip4_prefix = ip_prefix(ip4, 24, 0), ip6_prefix = ip_prefix(ip6, 0, 112);\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.keep": "KEEP", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.keep.markdown": "### KEEP\nLa commande `KEEP` permet de définir les colonnes qui seront renvoyées et l'ordre dans lequel elles le seront.\n\nPour limiter les colonnes retournées, utilisez une liste de noms de colonnes séparés par des virgules. Les colonnes sont renvoyées dans l'ordre indiqué :\n \n````\nFROM employees\n| KEEP first_name, last_name, height\n````\n\nPlutôt que de spécifier chaque colonne par son nom, vous pouvez utiliser des caractères génériques pour renvoyer toutes les colonnes dont le nom correspond à un modèle :\n\n````\nFROM employees\n| KEEP h*\n````\n\nLe caractère générique de l'astérisque (\"*\") placé de manière isolée transpose l'ensemble des colonnes qui ne correspondent pas aux autres arguments. La requête suivante renverra en premier lieu toutes les colonnes dont le nom commence par un h, puis toutes les autres colonnes :\n\n````\nFROM employees\n| KEEP h*, *\n````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.least": "LEAST", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.least.markdown": "\n\n ### LEAST\n Renvoie la valeur minimale de plusieurs colonnes. Cette fonction est similaire à `MV_MIN`. Toutefois, elle est destinée à être exécutée sur plusieurs colonnes à la fois.\n\n ````\n ROW a = 10, b = 20\n | EVAL l = LEAST(a, b)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.left": "LEFT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.left.markdown": "\n\n ### LEFT\n Renvoie la sous-chaîne qui extrait la \"longueur\" des caractères de la \"chaîne\" en partant de la gauche.\n\n ````\n FROM employees\n | KEEP last_name\n | EVAL left = LEFT(last_name, 3)\n | SORT last_name ASC\n | LIMIT 5\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.length": "LENGHT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.length.markdown": "\n\n ### LENGTH\n Renvoie la longueur des caractères d'une chaîne.\n\n ````\n FROM employees\n | KEEP first_name, last_name\n | EVAL fn_length = LENGTH(first_name)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.limit": "LIMIT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.limit.markdown": "### LIMIT\nLa commande de traitement `LIMIT` permet de restreindre le nombre de lignes :\n \n````\nFROM employees\n| LIMIT 5\n````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.locate": "LOCATE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.locate.markdown": "\n\n ### LOCATE\n Renvoie un entier qui indique la position d'une sous-chaîne de mots-clés dans une autre chaîne\n\n ````\n row a = \"hello\"\n | eval a_ll = locate(a, \"ll\")\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.log": "LOG", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.log.markdown": "\n\n ### LOG\n Renvoie le logarithme d'une valeur dans une base. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée.\n\n Les journaux de zéros, de nombres négatifs et de base 1 renvoient `null` ainsi qu'un avertissement.\n\n ````\n ROW base = 2.0, value = 8.0\n | EVAL s = LOG(base, value)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.log10": "LOG10", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.log10.markdown": "\n\n ### LOG10\n Renvoie le logarithme d'une valeur en base 10. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée.\n\n Les logs de 0 et de nombres négatifs renvoient `null` ainsi qu'un avertissement.\n\n ````\n ROW d = 1000.0 \n | EVAL s = LOG10(d)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ltrim": "LTRIM", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ltrim.markdown": "\n\n ### LTRIM\n Retire les espaces au début des chaînes.\n\n ````\n ROW message = \" some text \", color = \" red \"\n | EVAL message = LTRIM(message)\n | EVAL color = LTRIM(color)\n | EVAL message = CONCAT(\"'\", message, \"'\")\n | EVAL color = CONCAT(\"'\", color, \"'\")\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.markdown": "## ES|QL\n\nUne requête ES|QL (langage de requête Elasticsearch) se compose d'une série de commandes, séparées par une barre verticale : `|`. Chaque requête commence par une **commande source**, qui produit un tableau, habituellement avec des données issues d'Elasticsearch. \n\nUne commande source peut être suivie d'une ou plusieurs **commandes de traitement**. Les commandes de traitement peuvent modifier le tableau de sortie de la commande précédente en ajoutant, supprimant ou modifiant les lignes et les colonnes.\n\n````\nsource-command\n| processing-command1\n| processing-command2\n````\n\nLe résultat d'une requête est le tableau produit par la dernière commande de traitement. \n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_append": "MV_APPEND", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_append.markdown": "\n\n ### MV_APPEND\n Concatène les valeurs de deux champs à valeurs multiples.\n\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_avg": "MV_AVG", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_avg.markdown": "\n\n ### MV_AVG\n Convertit un champ multivalué en un champ à valeur unique comprenant la moyenne de toutes les valeurs.\n\n ````\n ROW a=[3, 5, 1, 6]\n | EVAL avg_a = MV_AVG(a)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_concat": "MV_CONCAT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_concat.markdown": "\n\n ### MV_CONCAT\n Convertit une expression de type chaîne multivalué en une colonne à valeur unique comprenant la concaténation de toutes les valeurs, séparées par un délimiteur.\n\n ````\n ROW a=[\"foo\", \"zoo\", \"bar\"]\n | EVAL j = MV_CONCAT(a, \", \")\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_count": "MV_COUNT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_count.markdown": "\n\n ### MV_COUNT\n Convertit une expression multivaluée en une colonne à valeur unique comprenant le total du nombre de valeurs.\n\n ````\n ROW a=[\"foo\", \"zoo\", \"bar\"]\n | EVAL count_a = MV_COUNT(a)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_dedupe": "MV_DEDUPE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_dedupe.markdown": "\n\n ### MV_DEDUPE\n Supprime les valeurs en doublon d'un champ multivalué.\n\n ````\n ROW a=[\"foo\", \"foo\", \"bar\", \"foo\"]\n | EVAL dedupe_a = MV_DEDUPE(a)\n ````\n Remarque : la fonction `MV_DEDUPE` est en mesure de trier les valeurs de la colonne, mais ne le fait pas systématiquement.\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_first": "MV_FIRST", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_first.markdown": "\n\n ### MV_FIRST\n Convertit une expression multivaluée en une colonne à valeur unique comprenant la\n première valeur. Ceci est particulièrement utile pour lire une fonction qui émet\n des colonnes multivaluées dans un ordre connu, comme `SPLIT`.\n\n L'ordre dans lequel les champs multivalués sont lus à partir\n du stockage sous-jacent n'est pas garanti. Il est *souvent* ascendant, mais ne vous y\n fiez pas. Si vous avez besoin de la valeur minimale, utilisez `MV_MIN` au lieu de\n `MV_FIRST`. `MV_MIN` comporte des optimisations pour les valeurs triées, il n'y a donc aucun\n avantage en matière de performances pour `MV_FIRST`.\n\n ````\n ROW a=\"foo;bar;baz\"\n | EVAL first_a = MV_FIRST(SPLIT(a, \";\"))\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_last": "MV_LAST", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_last.markdown": "\n\n ### MV_LAST\n Convertit une expression multivaluée en une colonne à valeur unique comprenant la dernière\n valeur. Ceci est particulièrement utile pour lire une fonction qui émet des champs multivalués\n dans un ordre connu, comme `SPLIT`.\n\n L'ordre dans lequel les champs multivalués sont lus à partir\n du stockage sous-jacent n'est pas garanti. Il est *souvent* ascendant, mais ne vous y\n fiez pas. Si vous avez besoin de la valeur maximale, utilisez `MV_MAX` au lieu de\n `MV_LAST`. `MV_MAX` comporte des optimisations pour les valeurs triées, il n'y a donc aucun\n avantage en matière de performances pour `MV_LAST`.\n\n ````\n ROW a=\"foo;bar;baz\"\n | EVAL last_a = MV_LAST(SPLIT(a, \";\"))\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_max": "MV_MAX", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_max.markdown": "\n\n ### MV_MAX\n Convertit une expression multivaluée en une colonne à valeur unique comprenant la valeur maximale.\n\n ````\n ROW a=[3, 5, 1]\n | EVAL max_a = MV_MAX(a)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_median": "MV_MEDIAN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_median.markdown": "\n\n ### MV_MEDIAN\n Convertit un champ multivalué en un champ à valeur unique comprenant la valeur médiane.\n\n ````\n ROW a=[3, 5, 1]\n | EVAL median_a = MV_MEDIAN(a)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_min": "MV_MIN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_min.markdown": "\n\n ### MV_MIN\n Convertit une expression multivaluée en une colonne à valeur unique comprenant la valeur minimale.\n\n ````\n ROW a=[2, 1]\n | EVAL min_a = MV_MIN(a)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_slice": "MV_SLICE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_slice.markdown": "\n\n ### MV_SLICE\n Renvoie un sous-ensemble du champ multivalué en utilisant les valeurs d'index de début et de fin.\n\n ````\n row a = [1, 2, 2, 3]\n | eval a1 = mv_slice(a, 1), a2 = mv_slice(a, 2, 3)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_sort": "MV_SORT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_sort.markdown": "\n\n ### MV_SORT\n Trie une expression multivaluée par ordre lexicographique.\n\n ````\n ROW a = [4, 2, -3, 2]\n | EVAL sa = mv_sort(a), sd = mv_sort(a, \"DESC\")\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_sum": "MV_SUM", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_sum.markdown": "\n\n ### MV_SUM\n Convertit un champ multivalué en un champ à valeur unique comprenant la somme de toutes les valeurs.\n\n ````\n ROW a=[3, 5, 6]\n | EVAL sum_a = MV_SUM(a)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_zip": "MV_ZIP", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_zip.markdown": "\n\n ### MV_ZIP\n Combine les valeurs de deux champs multivalués avec un délimiteur qui les relie.\n\n ````\n ROW a = [\"x\", \"y\", \"z\"], b = [\"1\", \"2\"]\n | EVAL c = mv_zip(a, b, \"-\")\n | KEEP a, b, c\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mvExpand": "MV_EXPAND", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mvExpand.markdown": "### MV_EXPAND\nLa commande de traitement `MV_EXPAND` développe les champs multivalués en indiquant une valeur par ligne et en dupliquant les autres champs : \n````\nROW a=[1,2,3], b=\"b\", j=[\"a\",\"b\"]\n| MV_EXPAND a\n````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.now": "NOW", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.now.markdown": "\n\n ### NOW\n Renvoie la date et l'heure actuelles.\n\n ````\n ROW current_date = NOW()\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.pi": "PI", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.pi.markdown": "\n\n ### PI\n Renvoie Pi, le rapport entre la circonférence et le diamètre d'un cercle.\n\n ````\n ROW PI()\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.pow": "POW", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.pow.markdown": "\n\n ### POW\n Renvoie la valeur d’une `base` élevée à la puissance d’un `exposant`.\n\n ````\n ROW base = 2.0, exponent = 2\n | EVAL result = POW(base, exponent)\n ````\n Remarque : Il est toujours possible de dépasser un résultat double ici ; dans ce cas, la valeur `null` sera renvoyée.\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.predicates": "valeurs NULL", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.predicates.markdown": "### Valeurs NULL\nPour une comparaison avec une valeur NULL, utilisez les attributs `IS NULL` et `IS NOT NULL` :\n\n````\nFROM employees\n| WHERE birth_date IS NULL\n| KEEP first_name, last_name\n| SORT first_name\n| LIMIT 3\n````\n\n````\nFROM employees\n| WHERE is_rehired IS NOT NULL\n| STATS count(emp_no)\n````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.rename": "RENAME", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.rename.markdown": "### RENAME\nUtilisez `RENAME` pour renommer une colonne en utilisant la syntaxe suivante :\n\n````\nRENAME AS \n````\n\nPar exemple :\n\n````\nFROM employees\n| KEEP first_name, last_name, still_hired\n| RENAME still_hired AS employed\n````\n\nSi une colonne portant le nouveau nom existe déjà, elle sera remplacée par la nouvelle colonne.\n\nPlusieurs colonnes peuvent être renommées à l'aide d'une seule commande `RENAME` :\n\n````\nFROM employees\n| KEEP first_name, last_name\n| RENAME first_name AS fn, last_name AS ln\n````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.repeat": "REPEAT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.repeat.markdown": "\n\n ### REPEAT\n Renvoie une chaîne construite par la concaténation de la `chaîne` avec elle-même, le `nombre` de fois spécifié.\n\n ````\n ROW a = \"Hello!\"\n | EVAL triple_a = REPEAT(a, 3);\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.replace": "REPLACE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.replace.markdown": "\n\n ### REPLACE\n La fonction remplace dans la chaîne `str` toutes les correspondances avec l'expression régulière `regex`\n par la chaîne de remplacement `newStr`.\n\n ````\n ROW str = \"Hello World\"\n | EVAL str = REPLACE(str, \"World\", \"Universe\")\n | KEEP str\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.right": "RIGHT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.right.markdown": "\n\n ### RIGHT\n Renvoie la sous-chaîne qui extrait la longueur des caractères de `str` en partant de la droite.\n\n ````\n FROM employees\n | KEEP last_name\n | EVAL right = RIGHT(last_name, 3)\n | SORT last_name ASC\n | LIMIT 5\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.round": "ROUND", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.round.markdown": "\n\n ### ROUND\n Arrondit un nombre au nombre spécifié de décimales.\n La valeur par défaut est 0, qui renvoie l'entier le plus proche. Si le\n nombre de décimales spécifié est négatif, la fonction arrondit au nombre de décimales à gauche\n de la virgule.\n\n ````\n FROM employees\n | KEEP first_name, last_name, height\n | EVAL height_ft = ROUND(height * 3.281, 1)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.row": "ROW", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.row.markdown": "### ROW\nLa commande source `ROW` renvoie une ligne contenant une ou plusieurs colonnes avec les valeurs que vous spécifiez. Cette commande peut s'avérer utile pour les tests.\n \n````\nROW a = 1, b = \"two\", c = null\n````\n\nUtilisez des crochets pour créer des colonnes à valeurs multiples :\n\n````\nROW a = [2, 1]\n````\n\nROW permet d'utiliser des fonctions :\n\n````\nROW a = ROUND(1.23, 0)\n````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.rtrim": "RTRIM", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.rtrim.markdown": "\n\n ### RTRIM\n Supprime les espaces à la fin des chaînes.\n\n ````\n ROW message = \" some text \", color = \" red \"\n | EVAL message = RTRIM(message)\n | EVAL color = RTRIM(color)\n | EVAL message = CONCAT(\"'\", message, \"'\")\n | EVAL color = CONCAT(\"'\", color, \"'\")\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.show": "SHOW", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.show.markdown": "### SHOW\nLa commande source `SHOW ` renvoie des informations sur le déploiement et ses capacités :\n\n* Utilisez `SHOW INFO` pour renvoyer la version du déploiement, la date de compilation et le hachage.\n* Utilisez `SHOW FUNCTIONS` pour renvoyer une liste de toutes les fonctions prises en charge et un résumé de chaque fonction.\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.signum": "SIGNUM", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.signum.markdown": "\n\n ### SIGNUM\n Renvoie le signe du nombre donné.\n Il renvoie `-1` pour les nombres négatifs, `0` pour `0` et `1` pour les nombres positifs.\n\n ````\n ROW d = 100.0\n | EVAL s = SIGNUM(d)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sin": "SIN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sin.markdown": "\n\n ### SIN\n Renvoie la fonction trigonométrique sinusoïdale d'un angle.\n\n ````\n ROW a=1.8 \n | EVAL sin=SIN(a)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sinh": "SINH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sinh.markdown": "\n\n ### SINH\n Renvoie le sinus hyperbolique d'un angle.\n\n ````\n ROW a=1.8 \n | EVAL sinh=SINH(a)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sort": "SORT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sort.markdown": "### SORT\nUtilisez la commande `SORT` pour trier les lignes sur un ou plusieurs champs :\n\n````\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height\n````\n\nL'ordre de tri par défaut est croissant. Définissez un ordre de tri explicite en utilisant `ASC` ou `DESC` :\n\n````\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height DESC\n````\n\nSi deux lignes disposent de la même clé de tri, l'ordre original sera préservé. Vous pouvez ajouter des expressions de tri pour départager les deux lignes :\n\n````\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height DESC, first_name ASC\n````\n\n#### valeurs `null`\nPar défaut, les valeurs `null` sont considérées comme étant supérieures à toutes les autres valeurs. Selon un ordre de tri croissant, les valeurs `null` sont classées en dernier. Selon un ordre de tri décroissant, les valeurs `null` sont classées en premier. Pour modifier cet ordre, utilisez `NULLS FIRST` ou `NULLS LAST` :\n\n````\nFROM employees\n| KEEP first_name, last_name, height\n| SORT first_name ASC NULLS FIRST\n````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.split": "SPLIT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.split.markdown": "\n\n ### SPLIT\n Divise une chaîne de valeur unique en plusieurs chaînes.\n\n ````\n ROW words=\"foo;bar;baz;qux;quux;corge\"\n | EVAL word = SPLIT(words, \";\")\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sqrt": "SQRT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sqrt.markdown": "\n\n ### SQRT\n Renvoie la racine carrée d'un nombre. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée.\n Les racines carrées des nombres négatifs et des infinis sont nulles.\n\n ````\n ROW d = 100.0\n | EVAL s = SQRT(d)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_contains": "ST_CONTAINS", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_contains.markdown": "\n\n ### ST_CONTAINS\n Renvoie si la première géométrie contient la deuxième géométrie.\n Il s'agit de l'inverse de la fonction `ST_WITHIN`.\n\n ````\n FROM airport_city_boundaries\n | WHERE ST_CONTAINS(city_boundary, TO_GEOSHAPE(\"POLYGON((109.35 18.3, 109.45 18.3, 109.45 18.4, 109.35 18.4, 109.35 18.3))\"))\n | KEEP abbrev, airport, region, city, city_location\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_disjoint": "ST_DISJOINT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_disjoint.markdown": "\n\n ### ST_DISJOINT\n Renvoie si les deux géométries ou colonnes géométriques sont disjointes.\n Il s'agit de l'inverse de la fonction `ST_INTERSECTS`.\n En termes mathématiques : ST_Disjoint(A, B) ⇔ A ⋂ B = ∅\n\n ````\n FROM airport_city_boundaries\n | WHERE ST_DISJOINT(city_boundary, TO_GEOSHAPE(\"POLYGON((-10 -60, 120 -60, 120 60, -10 60, -10 -60))\"))\n | KEEP abbrev, airport, region, city, city_location\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_distance": "ST_DISTANCE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_distance.markdown": "\n\n ### ST_DISTANCE\n Calcule la distance entre deux points.\n Pour les géométries cartésiennes, c’est la distance pythagoricienne dans les mêmes unités que les coordonnées d'origine.\n Pour les géométries géographiques, c’est la distance circulaire le long du grand cercle en mètres.\n\n ````\n Aéroports FROM\n | WHERE abbrev == \"CPH\"\n | EVAL distance = ST_DISTANCE(location, city_location)\n | KEEP abbrev, name, location, city_location, distance\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_intersects": "ST_INTERSECTS", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_intersects.markdown": "\n\n ### ST_INTERSECTS\n Renvoie `true` (vrai) si deux géométries se croisent.\n Elles se croisent si elles ont un point commun, y compris leurs points intérieurs\n (les points situés le long des lignes ou dans des polygones).\n Il s'agit de l'inverse de la fonction `ST_DISJOINT`.\n En termes mathématiques : ST_Intersects(A, B) ⇔ A ⋂ B ≠ ∅\n\n ````\n Aéroports FROM\n | WHERE ST_INTERSECTS(location, TO_GEOSHAPE(\"POLYGON((42 14, 43 14, 43 15, 42 15, 42 14))\"))\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_within": "ST_WITHIN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_within.markdown": "\n\n ### ST_WITHIN\n Renvoie si la première géométrie est à l'intérieur de la deuxième géométrie.\n Il s'agit de l'inverse de la fonction `ST_CONTAINS`.\n\n ````\n FROM airport_city_boundaries\n | WHERE ST_WITHIN(city_boundary, TO_GEOSHAPE(\"POLYGON((109.1 18.15, 109.6 18.15, 109.6 18.65, 109.1 18.65, 109.1 18.15))\"))\n | KEEP abbrev, airport, region, city, city_location\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_x": "ST_X", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_x.markdown": "\n\n ### ST_X\n Extrait la coordonnée `x` du point fourni.\n Si les points sont de type `geo_point`, cela revient à extraire la valeur de la `longitude`.\n\n ````\n ROW point = TO_GEOPOINT(\"POINT(42.97109629958868 14.7552534006536)\")\n | EVAL x = ST_X(point), y = ST_Y(point)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_y": "ST_Y", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_y.markdown": "\n\n ### ST_Y\n Extrait la coordonnée `y` du point fourni.\n Si les points sont de type `geo_point`, cela revient à extraire la valeur de la `latitude`.\n\n ````\n ROW point = TO_GEOPOINT(\"POINT(42.97109629958868 14.7552534006536)\")\n | EVAL x = ST_X(point), y = ST_Y(point)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.starts_with": "STARTS_WITH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.starts_with.markdown": "\n\n ### STARTS_WITH\n Renvoie un booléen qui indique si une chaîne de mot-clés débute par une autre chaîne.\n\n ````\n FROM employees\n | KEEP last_name\n | EVAL ln_S = STARTS_WITH(last_name, \"B\")\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.statsby": "STATS ... BY", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.statsby.markdown": "### STATS ... BY\nUtilisez `STATS ... BY` pour regrouper les lignes en fonction d'une valeur commune et calculer une ou plusieurs valeurs agrégées sur les lignes regroupées.\n\n**Exemples** :\n\n````\nFROM employees\n| STATS count = COUNT(emp_no) BY languages\n| SORT languages\n````\n\nSi `BY` est omis, le tableau de sortie contient exactement une ligne avec les agrégations appliquées sur l'ensemble des données :\n\n````\nFROM employees\n| STATS avg_lang = AVG(languages)\n````\n\nIl est possible de calculer plusieurs valeurs :\n\n````\nFROM employees\n| STATS avg_lang = AVG(languages), max_lang = MAX(languages)\n````\n\nIl est également possible d'effectuer des regroupements en fonction de plusieurs valeurs (uniquement pour les champs longs et les champs de la famille de mots-clés) :\n\n````\nFROM employees\n| EVAL hired = DATE_FORMAT(hire_date, \"YYYY\")\n| STATS avg_salary = AVG(salary) BY hired, languages.long\n| EVAL avg_salary = ROUND(avg_salary)\n| SORT hired, languages.long\n````\n\nConsultez la rubrique **Fonctions d'agrégation** pour obtenir la liste des fonctions pouvant être utilisées avec `STATS ... BY`.\n\nLes fonctions d'agrégation et les expressions de regroupement acceptent toutes deux d'autres fonctions. Ceci est utile pour utiliser `STATS...BY` sur des colonnes à valeur multiple. Par exemple, pour calculer l'évolution moyenne du salaire, vous pouvez utiliser `MV_AVG` pour faire la moyenne des multiples valeurs par employé, et utiliser le résultat avec la fonction `AVG` :\n\n````\nFROM employees\n| STATS avg_salary_change = AVG(MV_AVG(salary_change))\n````\n\nLe regroupement par expression est par exemple le regroupement des employés en fonction de la première lettre de leur nom de famille :\n\n````\nFROM employees\n| STATS my_count = COUNT() BY LEFT(last_name, 1)\n| SORT \"LEFT(last_name, 1)\"\n````\n\nIl n'est pas obligatoire d'indiquer le nom de la colonne de sortie. S'il n'est pas spécifié, le nouveau nom de la colonne est égal à l'expression. La requête suivante renvoie une colonne appelée `AVG(salary)` :\n\n````\nFROM employees\n| STATS AVG(salary)\n````\n\nComme ce nom contient des caractères spéciaux, il doit être placé entre deux caractères (`) lorsqu'il est utilisé dans des commandes suivantes :\n\n````\nFROM employees\n| STATS AVG(salary)\n| EVAL avg_salary_rounded = ROUND(\"AVG(salary)\")\n````\n\n**Remarque** : `STATS` sans aucun groupe est beaucoup plus rapide que l'ajout d'un groupe.\n\n**Remarque** : Le regroupement sur une seule expression est actuellement beaucoup plus optimisé que le regroupement sur plusieurs expressions.\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.stringOperators": "LIKE et RLIKE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.stringOperators.markdown": "### LIKE et RLIKE\nPour comparer des chaînes en utilisant des caractères génériques ou des expressions régulières, utilisez `LIKE` ou `RLIKE` :\n\nUtilisez `LIKE` pour faire correspondre des chaînes à l'aide de caractères génériques. Les caractères génériques suivants sont pris en charge :\n\n* `*` correspond à zéro caractère ou plus.\n* `?` correspond à un seul caractère.\n\n````\nFROM employees\n| WHERE first_name LIKE \"?b*\"\n| KEEP first_name, last_name\n````\n\nUtilisez `RLIKE` pour faire correspondre des chaînes à l'aide d'expressions régulières :\n\n````\nFROM employees\n| WHERE first_name RLIKE \".leja.*\"\n| KEEP first_name, last_name\n````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.substring": "SUBSTRING", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.substring.markdown": "\n\n ### SUBSTRING\n Renvoie la sous-chaîne d'une chaîne, délimitée en fonction d'une position de départ et d'une longueur facultative\n\n ````\n FROM employees\n | KEEP last_name\n | EVAL ln_sub = SUBSTRING(last_name, 1, 3)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.tan": "TAN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.tan.markdown": "\n\n ### TAN\n Renvoie la fonction trigonométrique Tangente d'un angle.\n\n ````\n ROW a=1.8 \n | EVAL tan=TAN(a)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.tanh": "TANH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.tanh.markdown": "\n\n ### TANH\n Renvoie la fonction hyperbolique Tangente d'un angle.\n\n ````\n ROW a=1.8 \n | EVAL tanh=TANH(a)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.tau": "TAU", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.tau.markdown": "\n\n ### TAU\n Renvoie le rapport entre la circonférence et le rayon d'un cercle.\n\n ````\n ROW TAU()\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_base64": "TO_BASE64", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_base64.markdown": "\n\n ### TO_BASE64\n Encode une chaîne en chaîne base64.\n\n ````\n row a = \"elastic\" \n | eval e = to_base64(a)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_boolean": "TO_BOOLEAN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_boolean.markdown": "\n\n ### TO_BOOLEAN\n Convertit une valeur d'entrée en une valeur booléenne.\n Une chaîne de valeur *true* sera convertie, sans tenir compte de la casse, en une valeur booléenne *true*.\n Pour toute autre valeur, y compris une chaîne vide, la fonction renverra *false*.\n La valeur numérique *0* sera convertie en *false*, toute autre valeur sera convertie en *true*.\n\n ````\n ROW str = [\"true\", \"TRuE\", \"false\", \"\", \"yes\", \"1\"]\n | EVAL bool = TO_BOOLEAN(str)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_cartesianpoint": "TO_CARTESIANPOINT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_cartesianpoint.markdown": "\n\n ### TO_CARTESIANPOINT\n Convertit la valeur d'une entrée en une valeur `cartesian_point`.\n Une chaîne ne sera convertie que si elle respecte le format WKT Point.\n\n ````\n ROW wkt = [\"POINT(4297.11 -1475.53)\", \"POINT(7580.93 2272.77)\"]\n | MV_EXPAND wkt\n | EVAL pt = TO_CARTESIANPOINT(wkt)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_cartesianshape": "TO_CARTESIANSHAPE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_cartesianshape.markdown": "\n\n ### TO_CARTESIANSHAPE\n Convertit une valeur d'entrée en une valeur `cartesian_shape`.\n Une chaîne ne sera convertie que si elle respecte le format WKT.\n\n ````\n ROW wkt = [\"POINT(4297.11 -1475.53)\", \"POLYGON ((3339584.72 1118889.97, 4452779.63 4865942.27, 2226389.81 4865942.27, 1113194.90 2273030.92, 3339584.72 1118889.97))\"]\n | MV_EXPAND wkt\n | EVAL geom = TO_CARTESIANSHAPE(wkt)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_datetime": "TO_DATETIME", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_datetime.markdown": "\n\n ### TO_DATETIME\n Convertit une valeur d'entrée en une valeur de date.\n Une chaîne ne sera convertie que si elle respecte le format `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'`.\n Pour convertir des dates vers d'autres formats, utilisez `DATE_PARSE`.\n\n ````\n ROW string = [\"1953-09-02T00:00:00.000Z\", \"1964-06-02T00:00:00.000Z\", \"1964-06-02 00:00:00\"]\n | EVAL datetime = TO_DATETIME(string)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_degrees": "TO_DEGREES", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_degrees.markdown": "\n\n ### TO_DEGREES\n Convertit un nombre en radians en degrés.\n\n ````\n ROW rad = [1.57, 3.14, 4.71]\n | EVAL deg = TO_DEGREES(rad)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_double": "TO_DOUBLE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_double.markdown": "\n\n ### TO_DOUBLE\n Convertit une valeur d'entrée en une valeur double. Si le paramètre d'entrée est de type date,\n sa valeur sera interprétée en millisecondes depuis l'heure Unix,\n convertie en double. Le booléen *true* sera converti en double *1.0*, et *false* en *0.0*.\n\n ````\n ROW str1 = \"5.20128E11\", str2 = \"foo\"\n | EVAL dbl = TO_DOUBLE(\"520128000000\"), dbl1 = TO_DOUBLE(str1), dbl2 = TO_DOUBLE(str2)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_geopoint": "TO_GEOPOINT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_geopoint.markdown": "\n\n ### TO_GEOPOINT\n Convertit une valeur d'entrée en une valeur `geo_point`.\n Une chaîne ne sera convertie que si elle respecte le format WKT Point.\n\n ````\n ROW wkt = \"POINT(42.97109630194 14.7552534413725)\"\n | EVAL pt = TO_GEOPOINT(wkt)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_geoshape": "TO_GEOSHAPE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_geoshape.markdown": "\n\n ### TO_GEOSHAPE\n Convertit une valeur d'entrée en une valeur `geo_shape`.\n Une chaîne ne sera convertie que si elle respecte le format WKT.\n\n ````\n ROW wkt = \"POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))\"\n | EVAL geom = TO_GEOSHAPE(wkt)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_integer": "TO_INTEGER", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_integer.markdown": "\n\n ### TO_INTEGER\n Convertit une valeur d'entrée en une valeur entière.\n Si le paramètre d'entrée est de type date, sa valeur sera interprétée en millisecondes\n depuis l'heure Unix, convertie en entier.\n Le booléen *true* sera converti en entier *1*, et *false* en *0*.\n\n ````\n ROW long = [5013792, 2147483647, 501379200000]\n | EVAL int = TO_INTEGER(long)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_ip": "TO_IP", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_ip.markdown": "\n\n ### TO_IP\n Convertit une chaîne d'entrée en valeur IP.\n\n ````\n ROW str1 = \"1.1.1.1\", str2 = \"foo\"\n | EVAL ip1 = TO_IP(str1), ip2 = TO_IP(str2)\n | WHERE CIDR_MATCH(ip1, \"1.0.0.0/8\")\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_long": "TO_LONG", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_long.markdown": "\n\n ### TO_LONG\n Convertit une valeur d'entrée en une valeur longue. Si le paramètre d'entrée est de type date,\n sa valeur sera interprétée en millisecondes depuis l'heure Unix, convertie en valeur longue.\n Le booléen *true* sera converti en valeur longue *1*, et *false* en *0*.\n\n ````\n ROW str1 = \"2147483648\", str2 = \"2147483648.2\", str3 = \"foo\"\n | EVAL long1 = TO_LONG(str1), long2 = TO_LONG(str2), long3 = TO_LONG(str3)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_lower": "TO_LOWER", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_lower.markdown": "\n\n ### TO_LOWER\n Renvoie une nouvelle chaîne représentant la chaîne d'entrée convertie en minuscules.\n\n ````\n ROW message = \"Some Text\"\n | EVAL message_lower = TO_LOWER(message)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_radians": "TO_RADIANS", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_radians.markdown": "\n\n ### TO_RADIANS\n Convertit un nombre en degrés en radians.\n\n ````\n ROW deg = [90.0, 180.0, 270.0]\n | EVAL rad = TO_RADIANS(deg)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_string": "TO_STRING", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_string.markdown": "\n\n ### TO_STRING\n Convertit une valeur d'entrée en une chaîne.\n\n ````\n ROW a=10\n | EVAL j = TO_STRING(a)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_unsigned_long": "TO_UNSIGNED_LONG", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_unsigned_long.markdown": "\n\n ### TO_UNSIGNED_LONG\n Convertit une valeur d'entrée en une valeur longue non signée. Si le paramètre d'entrée est de type date,\n sa valeur sera interprétée en millisecondes depuis l'heure Unix, convertie en valeur longue non signée.\n Le booléen *true* sera converti en valeur longue non signée *1*, et *false* en *0*.\n\n ````\n ROW str1 = \"2147483648\", str2 = \"2147483648.2\", str3 = \"foo\"\n | EVAL long1 = TO_UNSIGNED_LONG(str1), long2 = TO_ULONG(str2), long3 = TO_UL(str3)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_upper": "TO_UPPER", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_upper.markdown": "\n\n ### TO_UPPER\n Renvoie une nouvelle chaîne représentant la chaîne d'entrée convertie en majuscules.\n\n ````\n ROW message = \"Some Text\"\n | EVAL message_upper = TO_UPPER(message)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_version": "TO_VERSION", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_version.markdown": "\n\n ### TO_VERSION\n Convertit une chaîne d'entrée en une valeur de version.\n\n ````\n ROW v = TO_VERSION(\"1.2.3\")\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.trim": "TRIM", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.trim.markdown": "\n\n ### TRIM\n Supprime les espaces de début et de fin d'une chaîne.\n\n ````\n ROW message = \" some text \", color = \" red \"\n | EVAL message = TRIM(message)\n | EVAL color = TRIM(color)\n ````\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.where": "WHERE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.where.markdown": "### WHERE\nUtilisez `WHERE` afin d'obtenir un tableau qui comprend toutes les lignes du tableau d'entrée pour lesquelles la condition fournie est évaluée à `true` :\n \n````\nFROM employees\n| KEEP first_name, last_name, still_hired\n| WHERE still_hired == true\n````\n\n#### Opérateurs\n\nPour obtenir un aperçu des opérateurs pris en charge, consultez la section **Opérateurs**.\n\n#### Fonctions\n`WHERE` prend en charge diverses fonctions de calcul des valeurs. Pour en savoir plus, consultez la section **Fonctions**.\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationLabel": "Documentation", + "languageDocumentationPopover.documentationESQL.abs": "ABS", + "languageDocumentationPopover.documentationESQL.abs.markdown": "\n\n ### ABS\n Renvoie la valeur absolue.\n\n ````\n Numéro ROW = -1.0 \n | EVAL abs_number = ABS(number)\n ````\n ", + "languageDocumentationPopover.documentationESQL.acos": "ACOS", + "languageDocumentationPopover.documentationESQL.acos.markdown": "\n\n ### ACOS\n Renvoie l'arc cosinus de `n` sous forme d'angle, exprimé en radians.\n\n ````\n ROW a=.9\n | EVAL acos=ACOS(a)\n ````\n ", + "languageDocumentationPopover.documentationESQL.asin": "ASIN", + "languageDocumentationPopover.documentationESQL.asin.markdown": "\n\n ### ASIN\n Renvoie l'arc sinus de l'entrée\n expression numérique sous forme d'angle, exprimée en radians.\n\n ````\n ROW a=.9\n | EVAL asin=ASIN(a)\n ````\n ", + "languageDocumentationPopover.documentationESQL.atan": "ATAN", + "languageDocumentationPopover.documentationESQL.atan.markdown": "\n\n ### ATAN\n Renvoie l'arc tangente de l'entrée\n expression numérique sous forme d'angle, exprimée en radians.\n\n ````\n ROW a=.12.9\n | EVAL atan=ATAN(a)\n ````\n ", + "languageDocumentationPopover.documentationESQL.atan2": "ATAN2", + "languageDocumentationPopover.documentationESQL.atan2.markdown": "\n\n ### ATAN2\n L'angle entre l'axe positif des x et le rayon allant de\n l'origine au point (x , y) dans le plan cartésien, exprimée en radians.\n\n ````\n ROW y=12.9, x=.6\n | EVAL atan2=ATAN2(y, x)\n ````\n ", + "languageDocumentationPopover.documentationESQL.autoBucketFunction": "COMPARTIMENT", + "languageDocumentationPopover.documentationESQL.autoBucketFunction.markdown": "### COMPARTIMENT\nCréer des groupes de valeurs, des compartiments (\"buckets\"), à partir d'une entrée d'un numéro ou d'un horodatage. La taille des compartiments peut être fournie directement ou choisie selon une plage de valeurs et de décompte recommandée.\n\n`BUCKET` a deux modes de fonctionnement : \n\n1. Dans lequel la taille du compartiment est calculée selon la recommandation de décompte d'un compartiment (quatre paramètres) et une plage.\n2. Dans lequel la taille du compartiment est fournie directement (deux paramètres).\n\nAvec un nombre cible de compartiments, le début d'une plage et la fin d'une plage, `BUCKET` choisit une taille de compartiment appropriée afin de générer le nombre cible de compartiments ou moins.\n\nPar exemple, demander jusqu'à 20 compartiments pour une année organisera les données en intervalles mensuels :\n\n````\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hire_date = MV_SORT(VALUES(hire_date)) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT hire_date\n````\n\n**REMARQUE** : Le but n'est pas de fournir le nombre précis de compartiments, mais plutôt de sélectionner une plage qui fournit, tout au plus, le nombre cible de compartiments.\n\nVous pouvez combiner `BUCKET` avec une agrégation pour créer un histogramme :\n\n````\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_month = COUNT(*) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT month\n````\n\n**REMARQUE** : `BUCKET` ne crée pas de compartiments qui ne correspondent à aucun document. C'est pourquoi, dans l'exemple précédent, il manque 1985-03-01 ainsi que d'autres dates.\n\nDemander d'autres compartiments peut résulter en une plage réduite. Par exemple, demander jusqu'à 100 compartiments en un an résulte en des compartiments hebdomadaires :\n\n````\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_week = COUNT(*) BY week = BUCKET(hire_date, 100, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT week\n````\n\n**REMARQUE** : `AUTO_BUCKET` ne filtre aucune ligne. Il n'utilise que la plage fournie pour choisir une taille de compartiment appropriée. Pour les lignes dont la valeur se situe en dehors de la plage, il renvoie une valeur de compartiment qui correspond à un compartiment situé en dehors de la plage. Associez `BUCKET` à `WHERE` pour filtrer les lignes.\n\nSi la taille de compartiment désirée est connue à l'avance, fournissez-la comme second argument, en ignorant la plage :\n\n````\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_week = COUNT(*) BY week = BUCKET(hire_date, 1 week)\n| SORT week\n````\n\n**REMARQUE** : Lorsque vous fournissez la taille du compartiment comme second argument, ce dernier doit être une période temporelle ou une durée.\n\n`BUCKET` peut également être utilisé pour des champs numériques. Par exemple, pour créer un histogramme de salaire :\n\n````\nFROM employees\n| STATS COUNT(*) by bs = BUCKET(salary, 20, 25324, 74999)\n| SORT bs\n````\n\nContrairement à l'exemple précédent qui filtre intentionnellement sur une plage temporelle, vous n'avez pas souvent besoin de filtrer sur une plage numérique. Vous devez trouver les valeurs min et max séparément. ES|QL n'a pas encore de façon aisée d'effectuer cette opération automatiquement.\n\nLa plage peut être ignorée si la taille désirée de compartiment est connue à l'avance. Fournissez-la simplement comme second argument :\n\n````\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS c = COUNT(1) BY b = BUCKET(salary, 5000.)\n| SORT b\n````\n\n**REMARQUE** : Lorsque vous fournissez la taille du compartiment comme second argument, elle doit être de type à **virgule flottante**.\n\nVoici un exemple sur comment créer des compartiments horaires pour les dernières 24 heures, et calculer le nombre d'événements par heure :\n\n````\nFROM sample_data\n| WHERE @timestamp >= NOW() - 1 day and @timestamp < NOW()\n| STATS COUNT(*) BY bucket = BUCKET(@timestamp, 25, NOW() - 1 day, NOW())\n````\n\nVoici un exemple permettant de créer des compartiments mensuels pour l'année 1985, et calculer le salaire moyen par mois d'embauche :\n\n````\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS AVG(salary) BY bucket = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT bucket\n````\n\n`BUCKET` peut être utilisé pour les parties de groupage et d'agrégation de la commande `STATS …​ BY ...`, tant que la partie d'agrégation de la fonction est **référencée par un alias défini dans la partie de groupage**, ou que celle-ci est invoquée avec exactement la même expression.\n\nPar exemple :\n\n````\nFROM employees\n| STATS s1 = b1 + 1, s2 = BUCKET(salary / 1000 + 999, 50.) + 2 BY b1 = BUCKET(salary / 100 + 99, 50.), b2 = BUCKET(salary / 1000 + 999, 50.)\n| SORT b1, b2\n| KEEP s1, b1, s2, b2\n````\n ", + "languageDocumentationPopover.documentationESQL.binaryOperators": "Opérateurs binaires", + "languageDocumentationPopover.documentationESQL.binaryOperators.markdown": "### Opérateurs binaires\nLes opérateurs de comparaison binaire suivants sont pris en charge :\n\n* égalité : `==`\n* inégalité : `!=`\n* inférieur à : `<`\n* inférieur ou égal à : `<=`\n* supérieur à : `>`\n* supérieur ou égal à : `>=`\n* ajouter : `+`\n* soustraire : `-`\n* multiplier par : `*`\n* diviser par : `/`\n* module : `%`\n ", + "languageDocumentationPopover.documentationESQL.booleanOperators": "Opérateurs booléens", + "languageDocumentationPopover.documentationESQL.booleanOperators.markdown": "### Opérateurs booléens\nLes opérateurs booléens suivants sont pris en charge :\n\n* `AND`\n* `OR`\n* `NOT`\n ", + "languageDocumentationPopover.documentationESQL.bucket": "COMPARTIMENT", + "languageDocumentationPopover.documentationESQL.bucket.markdown": "\n\n ### COMPARTIMENT\n Créer des groupes de valeurs, des compartiments (\"buckets\"), à partir d'une entrée d'un numéro ou d'un horodatage.\n La taille des compartiments peut être fournie directement ou choisie selon une plage de valeurs et de décompte recommandée.\n\n ````\n FROM employees\n | WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n | STATS hire_date = MV_SORT(VALUES(hire_date)) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n | SORT hire_date\n ````\n ", + "languageDocumentationPopover.documentationESQL.case": "CASE", + "languageDocumentationPopover.documentationESQL.case.markdown": "\n\n ### CAS\n Accepte les paires de conditions et de valeurs. La fonction renvoie la valeur qui\n appartient à la première condition étant évaluée comme `true`.\n\n Si le nombre d'arguments est impair, le dernier argument est la valeur par défaut qui est\n renvoyée si aucune condition ne correspond. Si le nombre d'arguments est pair, et\n qu'aucune condition ne correspond, la fonction renvoie `null`.\n\n ````\n FROM employees\n | EVAL type = CASE(\n languages <= 1, \"monolingual\",\n languages <= 2, \"bilingual\",\n \"polyglot\")\n | KEEP emp_no, languages, type\n ````\n ", + "languageDocumentationPopover.documentationESQL.castOperator": "Cast (::)", + "languageDocumentationPopover.documentationESQL.castOperator.markdown": "### CAST (`::`)\nL'opérateur `::` fournit une syntaxe alternative pratique au type de converstion de fonction `TO_`.\n\nExemple :\n````\nROW ver = CONCAT((\"0\"::INT + 1)::STRING, \".2.3\")::VERSION\n````\n ", + "languageDocumentationPopover.documentationESQL.cbrt": "CBRT", + "languageDocumentationPopover.documentationESQL.cbrt.markdown": "\n\n ### CBRT\n Renvoie la racine cubique d'un nombre. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée.\n La racine cubique de l’infini est nulle.\n\n ````\n ROW d = 1000.0\n | EVAL c = cbrt(d)\n ````\n ", + "languageDocumentationPopover.documentationESQL.ceil": "CEIL", + "languageDocumentationPopover.documentationESQL.ceil.markdown": "\n\n ### CEIL\n Arrondir un nombre à l'entier supérieur.\n\n ```\n ROW a=1.8\n | EVAL a=CEIL(a)\n ```\n Remarque : Il s'agit d'un noop pour `long` (y compris non signé) et `integer`. Pour `double`, la fonction choisit la valeur `double` la plus proche de l'entier, de manière similaire à la méthode Math.ceil.\n ", + "languageDocumentationPopover.documentationESQL.cidr_match": "CIDR_MATCH", + "languageDocumentationPopover.documentationESQL.cidr_match.markdown": "\n\n ### CIDR_MATCH\n Renvoie `true` si l'IP fournie est contenue dans l'un des blocs CIDR fournis.\n\n ````\n FROM hosts \n | WHERE CIDR_MATCH(ip1, \"127.0.0.2/32\", \"127.0.0.3/32\") \n | KEEP card, host, ip0, ip1\n ````\n ", + "languageDocumentationPopover.documentationESQL.coalesce": "COALESCE", + "languageDocumentationPopover.documentationESQL.coalesce.markdown": "\n\n ### COALESCE\n Renvoie le premier de ses arguments qui n'est pas nul. Si tous les arguments sont nuls, `null` est renvoyé.\n\n ````\n ROW a=null, b=\"b\"\n | EVAL COALESCE(a, b)\n ````\n ", + "languageDocumentationPopover.documentationESQL.concat": "CONCAT", + "languageDocumentationPopover.documentationESQL.concat.markdown": "\n\n ### CONCAT\n Concatène deux ou plusieurs chaînes.\n\n ```\n FROM employees\n | KEEP first_name, last_name\n | EVAL fullname = CONCAT(first_name, \" \", last_name)\n ````\n ", + "languageDocumentationPopover.documentationESQL.cos": "COS", + "languageDocumentationPopover.documentationESQL.cos.markdown": "\n\n ### COS\n Renvoie le cosinus d'un angle.\n\n ````\n ROW a=1.8 \n | EVAL cos=COS(a)\n ````\n ", + "languageDocumentationPopover.documentationESQL.cosh": "COSH", + "languageDocumentationPopover.documentationESQL.cosh.markdown": "\n\n ### COSH\n Renvoie le cosinus hyperbolique d'un angle.\n\n ````\n ROW a=1.8 \n | EVAL cosh=COSH(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.date_diff": "DATE_DIFF", + "languageDocumentationPopover.documentationESQL.date_diff.markdown": "\n\n ### DATE_DIFF\n Soustrait le `startTimestamp` du `endTimestamp` et renvoie la différence en multiples `d'unité`.\n Si `startTimestamp` est postérieur à `endTimestamp`, des valeurs négatives sont renvoyées.\n\n ````\n ROW date1 = TO_DATETIME(\"2023-12-02T11:00:00.000Z\"), date2 = TO_DATETIME(\"2023-12-02T11:00:00.001Z\")\n | EVAL dd_ms = DATE_DIFF(\"microseconds\", date1, date2)\n ````\n ", + "languageDocumentationPopover.documentationESQL.date_extract": "DATE_EXTRACT", + "languageDocumentationPopover.documentationESQL.date_extract.markdown": "\n\n ### DATE_EXTRACT\n Extrait des parties d'une date, telles que l'année, le mois, le jour, l'heure.\n\n ````\n ROW date = DATE_PARSE(\"yyyy-MM-dd\", \"2022-05-06\")\n | EVAL year = DATE_EXTRACT(\"year\", date)\n ````\n ", + "languageDocumentationPopover.documentationESQL.date_format": "DATE_FORMAT", + "languageDocumentationPopover.documentationESQL.date_format.markdown": "\n\n ### DATE_FORMAT\n Renvoie une représentation sous forme de chaîne d'une date dans le format fourni.\n\n ````\n FROM employees\n | KEEP first_name, last_name, hire_date\n | EVAL hired = DATE_FORMAT(\"YYYY-MM-dd\", hire_date)\n ````\n ", + "languageDocumentationPopover.documentationESQL.date_parse": "DATE_PARSE", + "languageDocumentationPopover.documentationESQL.date_parse.markdown": "\n\n ### DATE_PARSE\n Renvoie une date en analysant le deuxième argument selon le format spécifié dans le premier argument.\n\n ````\n ROW date_string = \"2022-05-06\"\n | EVAL date = DATE_PARSE(\"yyyy-MM-dd\", date_string)\n ````\n ", + "languageDocumentationPopover.documentationESQL.date_trunc": "DATE_TRUNC", + "languageDocumentationPopover.documentationESQL.date_trunc.markdown": "\n\n ### DATE_TRUNC\n Arrondit une date à l'intervalle le plus proche.\n\n ```\n FROM employees\n | KEEP first_name, last_name, hire_date\n | EVAL year_hired = DATE_TRUNC(1 year, hire_date)\n ````\n ", + "languageDocumentationPopover.documentationESQL.dissect": "DISSECT", + "languageDocumentationPopover.documentationESQL.dissect.markdown": "### DISSECT\n`DISSECT` vous permet d'extraire des données structurées d'une chaîne. `DISSECT` compare la chaîne à un modèle basé sur les délimiteurs, et extrait les clés indiquées en tant que colonnes.\n\nPour obtenir la syntaxe des modèles \"dissect\", consultez [la documentation relative au processeur \"dissect\"](https://www.elastic.co/guide/en/elasticsearch/reference/current/dissect-processor.html).\n\n```\nROW a = \"1953-01-23T12:15:00Z - some text - 127.0.0.1\"\n| DISSECT a \"%'{Y}-%{M}-%{D}T%{h}:%{m}:%{s}Z - %{msg} - %{ip}'\"\n```` ", + "languageDocumentationPopover.documentationESQL.drop": "DROP", + "languageDocumentationPopover.documentationESQL.drop.markdown": "### DROP\nAfin de supprimer certaines colonnes d'un tableau, utilisez `DROP` :\n \n```\nFROM employees\n| DROP height\n```\n\nPlutôt que de spécifier chaque colonne par son nom, vous pouvez utiliser des caractères génériques pour supprimer toutes les colonnes dont le nom correspond à un modèle :\n\n```\nFROM employees\n| DROP height*\n````\n ", + "languageDocumentationPopover.documentationESQL.e": "E", + "languageDocumentationPopover.documentationESQL.e.markdown": "\n\n ### E\n Retourne le nombre d'Euler.\n\n ````\n ROW E()\n ````\n ", + "languageDocumentationPopover.documentationESQL.ends_with": "ENDS_WITH", + "languageDocumentationPopover.documentationESQL.ends_with.markdown": "\n\n ### ENDS_WITH\n Renvoie une valeur booléenne qui indique si une chaîne de mots-clés se termine par une autre chaîne.\n\n ````\n FROM employees\n | KEEP last_name\n | EVAL ln_E = ENDS_WITH(last_name, \"d\")\n ````\n ", + "languageDocumentationPopover.documentationESQL.enrich": "ENRICH", + "languageDocumentationPopover.documentationESQL.enrich.markdown": "### ENRICH\nVous pouvez utiliser `ENRICH` pour ajouter les données de vos index existants aux enregistrements entrants. Une fonction similaire à l'[enrichissement par ingestion](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html), mais qui fonctionne au moment de la requête.\n\n```\nROW language_code = \"1\"\n| ENRICH languages_policy\n```\n\n`ENRICH` requiert l'exécution d'une [politique d'enrichissement](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html#enrich-policy). La politique d'enrichissement définit un champ de correspondance (un champ clé) et un ensemble de champs d'enrichissement.\n\n`ENRICH` recherche les enregistrements dans l'[index d'enrichissement](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html#enrich-index) en se basant sur la valeur du champ de correspondance. La clé de correspondance dans l'ensemble de données d'entrée peut être définie en utilisant `ON `. Si elle n'est pas spécifiée, la correspondance sera effectuée sur un champ portant le même nom que le champ de correspondance défini dans la politique d'enrichissement.\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a\n```\n\nVous pouvez indiquer quels attributs (parmi ceux définis comme champs d'enrichissement dans la politique) doivent être ajoutés au résultat, en utilisant la syntaxe `WITH , ...`.\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a WITH language_name\n```\n\nLes attributs peuvent également être renommés à l'aide de la syntaxe `WITH new_name=`\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a WITH name = language_name\n````\n\nPar défaut (si aucun `WITH` n'est défini), `ENRICH` ajoute au résultat tous les champs d'enrichissement définis dans la politique d'enrichissement.\n\nEn cas de collision de noms, les champs nouvellement créés remplacent les champs existants.\n ", + "languageDocumentationPopover.documentationESQL.eval": "EVAL", + "languageDocumentationPopover.documentationESQL.eval.markdown": "### EVAL\n`EVAL` permet d'ajouter des colonnes :\n\n````\nFROM employees\n| KEEP first_name, last_name, height\n| EVAL height_feet = height * 3.281, height_cm = height * 100\n````\n\nSi la colonne indiquée existe déjà, la colonne existante sera supprimée et la nouvelle colonne sera ajoutée au tableau :\n\n````\nFROM employees\n| KEEP first_name, last_name, height\n| EVAL height = height * 3.281\n````\n\n#### Fonctions\n`EVAL` prend en charge diverses fonctions de calcul des valeurs. Pour en savoir plus, consultez les fonctions.\n ", + "languageDocumentationPopover.documentationESQL.floor": "FLOOR", + "languageDocumentationPopover.documentationESQL.floor.markdown": "\n\n ### FLOOR\n Arrondir un nombre à l'entier inférieur.\n\n ````\n ROW a=1.8\n | EVAL a=FLOOR(a)\n ````\n Remarque : Il s'agit d'un noop pour `long` (y compris non signé) et `integer`.\n Pour `double`, la fonction choisit la valeur `double` la plus proche de l'entier,\n de manière similaire à Math.floor.\n ", + "languageDocumentationPopover.documentationESQL.from": "FROM", + "languageDocumentationPopover.documentationESQL.from_base64": "FROM_BASE64", + "languageDocumentationPopover.documentationESQL.from_base64.markdown": "\n\n ### FROM_BASE64\n Décodez une chaîne base64.\n\n ````\n row a = \"ZWxhc3RpYw==\" \n | eval d = from_base64(a)\n ````\n ", + "languageDocumentationPopover.documentationESQL.from.markdown": "### FROM\nLa commande source `FROM` renvoie un tableau contenant jusqu'à 10 000 documents issus d'un flux de données, d'un index ou d'un alias. Chaque ligne du tableau obtenu correspond à un document. Chaque colonne correspond à un champ et est accessible par le nom de ce champ.\n\n````\nFROM employees\n````\n\nVous pouvez utiliser des [calculs impliquant des dates](https://www.elastic.co/guide/en/elasticsearch/reference/current/api-conventions.html#api-date-math-index-names) pour désigner les indices, les alias et les flux de données. Cela peut s'avérer utile pour les données temporelles.\n\nUtilisez des listes séparées par des virgules ou des caractères génériques pour rechercher plusieurs flux de données, indices ou alias :\n\n````\nFROM employees-00001,employees-*\n````\n\n#### Métadonnées\n\nES|QL peut accéder aux champs de métadonnées suivants :\n\n* `_index` : l'index auquel appartient le document. Le champ est du type `keyword`.\n* `_id` : l'identifiant du document source. Le champ est du type `keyword`.\n* `_id` : la version du document source. Le champ est du type `long`.\n\nUtilisez la directive `METADATA` pour activer les champs de métadonnées :\n\n````\nFROM index [METADATA _index, _id]\n````\n\nLes champs de métadonnées ne sont disponibles que si la source des données est un index. Par conséquent, `FROM` est la seule commande source qui prend en charge la directive `METADATA`.\n\nUne fois activés, les champs sont disponibles pour les commandes de traitement suivantes, tout comme les autres champs de l'index :\n\n````\nFROM ul_logs, apps [METADATA _index, _version]\n| WHERE id IN (13, 14) AND _version == 1\n| EVAL key = CONCAT(_index, \"_\", TO_STR(id))\n| SORT id, _index\n| KEEP id, _index, _version, key\n````\n\nDe même, comme pour les champs d'index, une fois l'agrégation effectuée, un champ de métadonnées ne sera plus accessible aux commandes suivantes, sauf s'il est utilisé comme champ de regroupement :\n\n````\nFROM employees [METADATA _index, _id]\n| STATS max = MAX(emp_no) BY _index\n````\n ", + "languageDocumentationPopover.documentationESQL.greatest": "GREATEST", + "languageDocumentationPopover.documentationESQL.greatest.markdown": "\n\n ### GREATEST\n Renvoie la valeur maximale de plusieurs colonnes. Similaire à `MV_MAX`\n sauf que ceci est destiné à une exécution sur plusieurs colonnes à la fois.\n\n ````\n ROW a = 10, b = 20\n | EVAL g = GREATEST(a, b)\n ````\n Remarque : Lorsque cette fonction est exécutée sur les champs `keyword` ou `text`, elle renvoie la dernière chaîne dans l'ordre alphabétique. Lorsqu'elle est exécutée sur des colonnes `boolean`, elle renvoie `true` si l'une des valeurs l'est.\n ", + "languageDocumentationPopover.documentationESQL.grok": "GROK", + "languageDocumentationPopover.documentationESQL.grok.markdown": "### GROK\n`GROK` vous permet d'extraire des données structurées d'une chaîne. `GROK` compare la chaîne à des modèles, sur la base d’expressions régulières, et extrait les modèles indiqués en tant que colonnes.\n\nPour obtenir la syntaxe des modèles \"grok\", consultez [la documentation relative au processeur \"grok\"](https://www.elastic.co/guide/en/elasticsearch/reference/current/grok-processor.html).\n\n````\nROW a = \"12 15.5 15.6 true\"\n| GROK a \"%'{NUMBER:b:int}' %'{NUMBER:c:float}' %'{NUMBER:d:double}' %'{WORD:e:boolean}'\"\n````\n ", + "languageDocumentationPopover.documentationESQL.inOperator": "IN", + "languageDocumentationPopover.documentationESQL.inOperator.markdown": "### IN\nL'opérateur `IN` permet de tester si un champ ou une expression est égal à un élément d'une liste de littéraux, de champs ou d'expressions :\n\n````\nROW a = 1, b = 4, c = 3\n| WHERE c-a IN (3, b / 2, a)\n````\n ", + "languageDocumentationPopover.documentationESQL.ip_prefix": "IP_PREFIX", + "languageDocumentationPopover.documentationESQL.ip_prefix.markdown": "\n\n ### IP_PREFIX\n Tronque une adresse IP à une longueur de préfixe donnée.\n\n ````\n row ip4 = to_ip(\"1.2.3.4\"), ip6 = to_ip(\"fe80::cae2:65ff:fece:feb9\")\n | eval ip4_prefix = ip_prefix(ip4, 24, 0), ip6_prefix = ip_prefix(ip6, 0, 112);\n ````\n ", + "languageDocumentationPopover.documentationESQL.keep": "KEEP", + "languageDocumentationPopover.documentationESQL.keep.markdown": "### KEEP\nLa commande `KEEP` permet de définir les colonnes qui seront renvoyées et l'ordre dans lequel elles le seront.\n\nPour limiter les colonnes retournées, utilisez une liste de noms de colonnes séparés par des virgules. Les colonnes sont renvoyées dans l'ordre indiqué :\n \n````\nFROM employees\n| KEEP first_name, last_name, height\n````\n\nPlutôt que de spécifier chaque colonne par son nom, vous pouvez utiliser des caractères génériques pour renvoyer toutes les colonnes dont le nom correspond à un modèle :\n\n````\nFROM employees\n| KEEP h*\n````\n\nLe caractère générique de l'astérisque (\"*\") placé de manière isolée transpose l'ensemble des colonnes qui ne correspondent pas aux autres arguments. La requête suivante renverra en premier lieu toutes les colonnes dont le nom commence par un h, puis toutes les autres colonnes :\n\n````\nFROM employees\n| KEEP h*, *\n````\n ", + "languageDocumentationPopover.documentationESQL.least": "LEAST", + "languageDocumentationPopover.documentationESQL.least.markdown": "\n\n ### LEAST\n Renvoie la valeur minimale de plusieurs colonnes. Cette fonction est similaire à `MV_MIN`. Toutefois, elle est destinée à être exécutée sur plusieurs colonnes à la fois.\n\n ````\n ROW a = 10, b = 20\n | EVAL l = LEAST(a, b)\n ````\n ", + "languageDocumentationPopover.documentationESQL.left": "LEFT", + "languageDocumentationPopover.documentationESQL.left.markdown": "\n\n ### LEFT\n Renvoie la sous-chaîne qui extrait la \"longueur\" des caractères de la \"chaîne\" en partant de la gauche.\n\n ````\n FROM employees\n | KEEP last_name\n | EVAL left = LEFT(last_name, 3)\n | SORT last_name ASC\n | LIMIT 5\n ````\n ", + "languageDocumentationPopover.documentationESQL.length": "LENGHT", + "languageDocumentationPopover.documentationESQL.length.markdown": "\n\n ### LENGTH\n Renvoie la longueur des caractères d'une chaîne.\n\n ````\n FROM employees\n | KEEP first_name, last_name\n | EVAL fn_length = LENGTH(first_name)\n ````\n ", + "languageDocumentationPopover.documentationESQL.limit": "LIMIT", + "languageDocumentationPopover.documentationESQL.limit.markdown": "### LIMIT\nLa commande de traitement `LIMIT` permet de restreindre le nombre de lignes :\n \n````\nFROM employees\n| LIMIT 5\n````\n ", + "languageDocumentationPopover.documentationESQL.locate": "LOCATE", + "languageDocumentationPopover.documentationESQL.locate.markdown": "\n\n ### LOCATE\n Renvoie un entier qui indique la position d'une sous-chaîne de mots-clés dans une autre chaîne\n\n ````\n row a = \"hello\"\n | eval a_ll = locate(a, \"ll\")\n ````\n ", + "languageDocumentationPopover.documentationESQL.log": "LOG", + "languageDocumentationPopover.documentationESQL.log.markdown": "\n\n ### LOG\n Renvoie le logarithme d'une valeur dans une base. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée.\n\n Les journaux de zéros, de nombres négatifs et de base 1 renvoient `null` ainsi qu'un avertissement.\n\n ````\n ROW base = 2.0, value = 8.0\n | EVAL s = LOG(base, value)\n ````\n ", + "languageDocumentationPopover.documentationESQL.log10": "LOG10", + "languageDocumentationPopover.documentationESQL.log10.markdown": "\n\n ### LOG10\n Renvoie le logarithme d'une valeur en base 10. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée.\n\n Les logs de 0 et de nombres négatifs renvoient `null` ainsi qu'un avertissement.\n\n ````\n ROW d = 1000.0 \n | EVAL s = LOG10(d)\n ````\n ", + "languageDocumentationPopover.documentationESQL.ltrim": "LTRIM", + "languageDocumentationPopover.documentationESQL.ltrim.markdown": "\n\n ### LTRIM\n Retire les espaces au début des chaînes.\n\n ````\n ROW message = \" some text \", color = \" red \"\n | EVAL message = LTRIM(message)\n | EVAL color = LTRIM(color)\n | EVAL message = CONCAT(\"'\", message, \"'\")\n | EVAL color = CONCAT(\"'\", color, \"'\")\n ````\n ", + "languageDocumentationPopover.documentationESQL.markdown": "## ES|QL\n\nUne requête ES|QL (langage de requête Elasticsearch) se compose d'une série de commandes, séparées par une barre verticale : `|`. Chaque requête commence par une **commande source**, qui produit un tableau, habituellement avec des données issues d'Elasticsearch. \n\nUne commande source peut être suivie d'une ou plusieurs **commandes de traitement**. Les commandes de traitement peuvent modifier le tableau de sortie de la commande précédente en ajoutant, supprimant ou modifiant les lignes et les colonnes.\n\n````\nsource-command\n| processing-command1\n| processing-command2\n````\n\nLe résultat d'une requête est le tableau produit par la dernière commande de traitement. \n ", + "languageDocumentationPopover.documentationESQL.mv_append": "MV_APPEND", + "languageDocumentationPopover.documentationESQL.mv_append.markdown": "\n\n ### MV_APPEND\n Concatène les valeurs de deux champs à valeurs multiples.\n\n ", + "languageDocumentationPopover.documentationESQL.mv_avg": "MV_AVG", + "languageDocumentationPopover.documentationESQL.mv_avg.markdown": "\n\n ### MV_AVG\n Convertit un champ multivalué en un champ à valeur unique comprenant la moyenne de toutes les valeurs.\n\n ````\n ROW a=[3, 5, 1, 6]\n | EVAL avg_a = MV_AVG(a)\n ````\n ", + "languageDocumentationPopover.documentationESQL.mv_concat": "MV_CONCAT", + "languageDocumentationPopover.documentationESQL.mv_concat.markdown": "\n\n ### MV_CONCAT\n Convertit une expression de type chaîne multivalué en une colonne à valeur unique comprenant la concaténation de toutes les valeurs, séparées par un délimiteur.\n\n ````\n ROW a=[\"foo\", \"zoo\", \"bar\"]\n | EVAL j = MV_CONCAT(a, \", \")\n ````\n ", + "languageDocumentationPopover.documentationESQL.mv_count": "MV_COUNT", + "languageDocumentationPopover.documentationESQL.mv_count.markdown": "\n\n ### MV_COUNT\n Convertit une expression multivaluée en une colonne à valeur unique comprenant le total du nombre de valeurs.\n\n ````\n ROW a=[\"foo\", \"zoo\", \"bar\"]\n | EVAL count_a = MV_COUNT(a)\n ````\n ", + "languageDocumentationPopover.documentationESQL.mv_dedupe": "MV_DEDUPE", + "languageDocumentationPopover.documentationESQL.mv_dedupe.markdown": "\n\n ### MV_DEDUPE\n Supprime les valeurs en doublon d'un champ multivalué.\n\n ````\n ROW a=[\"foo\", \"foo\", \"bar\", \"foo\"]\n | EVAL dedupe_a = MV_DEDUPE(a)\n ````\n Remarque : la fonction `MV_DEDUPE` est en mesure de trier les valeurs de la colonne, mais ne le fait pas systématiquement.\n ", + "languageDocumentationPopover.documentationESQL.mv_first": "MV_FIRST", + "languageDocumentationPopover.documentationESQL.mv_first.markdown": "\n\n ### MV_FIRST\n Convertit une expression multivaluée en une colonne à valeur unique comprenant la\n première valeur. Ceci est particulièrement utile pour lire une fonction qui émet\n des colonnes multivaluées dans un ordre connu, comme `SPLIT`.\n\n L'ordre dans lequel les champs multivalués sont lus à partir\n du stockage sous-jacent n'est pas garanti. Il est *souvent* ascendant, mais ne vous y\n fiez pas. Si vous avez besoin de la valeur minimale, utilisez `MV_MIN` au lieu de\n `MV_FIRST`. `MV_MIN` comporte des optimisations pour les valeurs triées, il n'y a donc aucun\n avantage en matière de performances pour `MV_FIRST`.\n\n ````\n ROW a=\"foo;bar;baz\"\n | EVAL first_a = MV_FIRST(SPLIT(a, \";\"))\n ````\n ", + "languageDocumentationPopover.documentationESQL.mv_last": "MV_LAST", + "languageDocumentationPopover.documentationESQL.mv_last.markdown": "\n\n ### MV_LAST\n Convertit une expression multivaluée en une colonne à valeur unique comprenant la dernière\n valeur. Ceci est particulièrement utile pour lire une fonction qui émet des champs multivalués\n dans un ordre connu, comme `SPLIT`.\n\n L'ordre dans lequel les champs multivalués sont lus à partir\n du stockage sous-jacent n'est pas garanti. Il est *souvent* ascendant, mais ne vous y\n fiez pas. Si vous avez besoin de la valeur maximale, utilisez `MV_MAX` au lieu de\n `MV_LAST`. `MV_MAX` comporte des optimisations pour les valeurs triées, il n'y a donc aucun\n avantage en matière de performances pour `MV_LAST`.\n\n ````\n ROW a=\"foo;bar;baz\"\n | EVAL last_a = MV_LAST(SPLIT(a, \";\"))\n ````\n ", + "languageDocumentationPopover.documentationESQL.mv_max": "MV_MAX", + "languageDocumentationPopover.documentationESQL.mv_max.markdown": "\n\n ### MV_MAX\n Convertit une expression multivaluée en une colonne à valeur unique comprenant la valeur maximale.\n\n ````\n ROW a=[3, 5, 1]\n | EVAL max_a = MV_MAX(a)\n ````\n ", + "languageDocumentationPopover.documentationESQL.mv_median": "MV_MEDIAN", + "languageDocumentationPopover.documentationESQL.mv_median.markdown": "\n\n ### MV_MEDIAN\n Convertit un champ multivalué en un champ à valeur unique comprenant la valeur médiane.\n\n ````\n ROW a=[3, 5, 1]\n | EVAL median_a = MV_MEDIAN(a)\n ````\n ", + "languageDocumentationPopover.documentationESQL.mv_min": "MV_MIN", + "languageDocumentationPopover.documentationESQL.mv_min.markdown": "\n\n ### MV_MIN\n Convertit une expression multivaluée en une colonne à valeur unique comprenant la valeur minimale.\n\n ````\n ROW a=[2, 1]\n | EVAL min_a = MV_MIN(a)\n ````\n ", + "languageDocumentationPopover.documentationESQL.mv_slice": "MV_SLICE", + "languageDocumentationPopover.documentationESQL.mv_slice.markdown": "\n\n ### MV_SLICE\n Renvoie un sous-ensemble du champ multivalué en utilisant les valeurs d'index de début et de fin.\n\n ````\n row a = [1, 2, 2, 3]\n | eval a1 = mv_slice(a, 1), a2 = mv_slice(a, 2, 3)\n ````\n ", + "languageDocumentationPopover.documentationESQL.mv_sort": "MV_SORT", + "languageDocumentationPopover.documentationESQL.mv_sort.markdown": "\n\n ### MV_SORT\n Trie une expression multivaluée par ordre lexicographique.\n\n ````\n ROW a = [4, 2, -3, 2]\n | EVAL sa = mv_sort(a), sd = mv_sort(a, \"DESC\")\n ````\n ", + "languageDocumentationPopover.documentationESQL.mv_sum": "MV_SUM", + "languageDocumentationPopover.documentationESQL.mv_sum.markdown": "\n\n ### MV_SUM\n Convertit un champ multivalué en un champ à valeur unique comprenant la somme de toutes les valeurs.\n\n ````\n ROW a=[3, 5, 6]\n | EVAL sum_a = MV_SUM(a)\n ````\n ", + "languageDocumentationPopover.documentationESQL.mv_zip": "MV_ZIP", + "languageDocumentationPopover.documentationESQL.mv_zip.markdown": "\n\n ### MV_ZIP\n Combine les valeurs de deux champs multivalués avec un délimiteur qui les relie.\n\n ````\n ROW a = [\"x\", \"y\", \"z\"], b = [\"1\", \"2\"]\n | EVAL c = mv_zip(a, b, \"-\")\n | KEEP a, b, c\n ````\n ", + "languageDocumentationPopover.documentationESQL.mvExpand": "MV_EXPAND", + "languageDocumentationPopover.documentationESQL.mvExpand.markdown": "### MV_EXPAND\nLa commande de traitement `MV_EXPAND` développe les champs multivalués en indiquant une valeur par ligne et en dupliquant les autres champs : \n````\nROW a=[1,2,3], b=\"b\", j=[\"a\",\"b\"]\n| MV_EXPAND a\n````\n ", + "languageDocumentationPopover.documentationESQL.now": "NOW", + "languageDocumentationPopover.documentationESQL.now.markdown": "\n\n ### NOW\n Renvoie la date et l'heure actuelles.\n\n ````\n ROW current_date = NOW()\n ````\n ", + "languageDocumentationPopover.documentationESQL.pi": "PI", + "languageDocumentationPopover.documentationESQL.pi.markdown": "\n\n ### PI\n Renvoie Pi, le rapport entre la circonférence et le diamètre d'un cercle.\n\n ````\n ROW PI()\n ````\n ", + "languageDocumentationPopover.documentationESQL.pow": "POW", + "languageDocumentationPopover.documentationESQL.pow.markdown": "\n\n ### POW\n Renvoie la valeur d’une `base` élevée à la puissance d’un `exposant`.\n\n ````\n ROW base = 2.0, exponent = 2\n | EVAL result = POW(base, exponent)\n ````\n Remarque : Il est toujours possible de dépasser un résultat double ici ; dans ce cas, la valeur `null` sera renvoyée.\n ", + "languageDocumentationPopover.documentationESQL.predicates": "valeurs NULL", + "languageDocumentationPopover.documentationESQL.predicates.markdown": "### Valeurs NULL\nPour une comparaison avec une valeur NULL, utilisez les attributs `IS NULL` et `IS NOT NULL` :\n\n````\nFROM employees\n| WHERE birth_date IS NULL\n| KEEP first_name, last_name\n| SORT first_name\n| LIMIT 3\n````\n\n````\nFROM employees\n| WHERE is_rehired IS NOT NULL\n| STATS count(emp_no)\n````\n ", + "languageDocumentationPopover.documentationESQL.rename": "RENAME", + "languageDocumentationPopover.documentationESQL.rename.markdown": "### RENAME\nUtilisez `RENAME` pour renommer une colonne en utilisant la syntaxe suivante :\n\n````\nRENAME AS \n````\n\nPar exemple :\n\n````\nFROM employees\n| KEEP first_name, last_name, still_hired\n| RENAME still_hired AS employed\n````\n\nSi une colonne portant le nouveau nom existe déjà, elle sera remplacée par la nouvelle colonne.\n\nPlusieurs colonnes peuvent être renommées à l'aide d'une seule commande `RENAME` :\n\n````\nFROM employees\n| KEEP first_name, last_name\n| RENAME first_name AS fn, last_name AS ln\n````\n ", + "languageDocumentationPopover.documentationESQL.repeat": "REPEAT", + "languageDocumentationPopover.documentationESQL.repeat.markdown": "\n\n ### REPEAT\n Renvoie une chaîne construite par la concaténation de la `chaîne` avec elle-même, le `nombre` de fois spécifié.\n\n ````\n ROW a = \"Hello!\"\n | EVAL triple_a = REPEAT(a, 3);\n ````\n ", + "languageDocumentationPopover.documentationESQL.replace": "REPLACE", + "languageDocumentationPopover.documentationESQL.replace.markdown": "\n\n ### REPLACE\n La fonction remplace dans la chaîne `str` toutes les correspondances avec l'expression régulière `regex`\n par la chaîne de remplacement `newStr`.\n\n ````\n ROW str = \"Hello World\"\n | EVAL str = REPLACE(str, \"World\", \"Universe\")\n | KEEP str\n ````\n ", + "languageDocumentationPopover.documentationESQL.right": "RIGHT", + "languageDocumentationPopover.documentationESQL.right.markdown": "\n\n ### RIGHT\n Renvoie la sous-chaîne qui extrait la longueur des caractères de `str` en partant de la droite.\n\n ````\n FROM employees\n | KEEP last_name\n | EVAL right = RIGHT(last_name, 3)\n | SORT last_name ASC\n | LIMIT 5\n ````\n ", + "languageDocumentationPopover.documentationESQL.round": "ROUND", + "languageDocumentationPopover.documentationESQL.round.markdown": "\n\n ### ROUND\n Arrondit un nombre au nombre spécifié de décimales.\n La valeur par défaut est 0, qui renvoie l'entier le plus proche. Si le\n nombre de décimales spécifié est négatif, la fonction arrondit au nombre de décimales à gauche\n de la virgule.\n\n ````\n FROM employees\n | KEEP first_name, last_name, height\n | EVAL height_ft = ROUND(height * 3.281, 1)\n ````\n ", + "languageDocumentationPopover.documentationESQL.row": "ROW", + "languageDocumentationPopover.documentationESQL.row.markdown": "### ROW\nLa commande source `ROW` renvoie une ligne contenant une ou plusieurs colonnes avec les valeurs que vous spécifiez. Cette commande peut s'avérer utile pour les tests.\n \n````\nROW a = 1, b = \"two\", c = null\n````\n\nUtilisez des crochets pour créer des colonnes à valeurs multiples :\n\n````\nROW a = [2, 1]\n````\n\nROW permet d'utiliser des fonctions :\n\n````\nROW a = ROUND(1.23, 0)\n````\n ", + "languageDocumentationPopover.documentationESQL.rtrim": "RTRIM", + "languageDocumentationPopover.documentationESQL.rtrim.markdown": "\n\n ### RTRIM\n Supprime les espaces à la fin des chaînes.\n\n ````\n ROW message = \" some text \", color = \" red \"\n | EVAL message = RTRIM(message)\n | EVAL color = RTRIM(color)\n | EVAL message = CONCAT(\"'\", message, \"'\")\n | EVAL color = CONCAT(\"'\", color, \"'\")\n ````\n ", + "languageDocumentationPopover.documentationESQL.show": "SHOW", + "languageDocumentationPopover.documentationESQL.show.markdown": "### SHOW\nLa commande source `SHOW ` renvoie des informations sur le déploiement et ses capacités :\n\n* Utilisez `SHOW INFO` pour renvoyer la version du déploiement, la date de compilation et le hachage.\n* Utilisez `SHOW FUNCTIONS` pour renvoyer une liste de toutes les fonctions prises en charge et un résumé de chaque fonction.\n ", + "languageDocumentationPopover.documentationESQL.signum": "SIGNUM", + "languageDocumentationPopover.documentationESQL.signum.markdown": "\n\n ### SIGNUM\n Renvoie le signe du nombre donné.\n Il renvoie `-1` pour les nombres négatifs, `0` pour `0` et `1` pour les nombres positifs.\n\n ````\n ROW d = 100.0\n | EVAL s = SIGNUM(d)\n ````\n ", + "languageDocumentationPopover.documentationESQL.sin": "SIN", + "languageDocumentationPopover.documentationESQL.sin.markdown": "\n\n ### SIN\n Renvoie la fonction trigonométrique sinusoïdale d'un angle.\n\n ````\n ROW a=1.8 \n | EVAL sin=SIN(a)\n ````\n ", + "languageDocumentationPopover.documentationESQL.sinh": "SINH", + "languageDocumentationPopover.documentationESQL.sinh.markdown": "\n\n ### SINH\n Renvoie le sinus hyperbolique d'un angle.\n\n ````\n ROW a=1.8 \n | EVAL sinh=SINH(a)\n ````\n ", + "languageDocumentationPopover.documentationESQL.sort": "SORT", + "languageDocumentationPopover.documentationESQL.sort.markdown": "### SORT\nUtilisez la commande `SORT` pour trier les lignes sur un ou plusieurs champs :\n\n````\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height\n````\n\nL'ordre de tri par défaut est croissant. Définissez un ordre de tri explicite en utilisant `ASC` ou `DESC` :\n\n````\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height DESC\n````\n\nSi deux lignes disposent de la même clé de tri, l'ordre original sera préservé. Vous pouvez ajouter des expressions de tri pour départager les deux lignes :\n\n````\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height DESC, first_name ASC\n````\n\n#### valeurs `null`\nPar défaut, les valeurs `null` sont considérées comme étant supérieures à toutes les autres valeurs. Selon un ordre de tri croissant, les valeurs `null` sont classées en dernier. Selon un ordre de tri décroissant, les valeurs `null` sont classées en premier. Pour modifier cet ordre, utilisez `NULLS FIRST` ou `NULLS LAST` :\n\n````\nFROM employees\n| KEEP first_name, last_name, height\n| SORT first_name ASC NULLS FIRST\n````\n ", + "languageDocumentationPopover.documentationESQL.split": "SPLIT", + "languageDocumentationPopover.documentationESQL.split.markdown": "\n\n ### SPLIT\n Divise une chaîne de valeur unique en plusieurs chaînes.\n\n ````\n ROW words=\"foo;bar;baz;qux;quux;corge\"\n | EVAL word = SPLIT(words, \";\")\n ````\n ", + "languageDocumentationPopover.documentationESQL.sqrt": "SQRT", + "languageDocumentationPopover.documentationESQL.sqrt.markdown": "\n\n ### SQRT\n Renvoie la racine carrée d'un nombre. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée.\n Les racines carrées des nombres négatifs et des infinis sont nulles.\n\n ````\n ROW d = 100.0\n | EVAL s = SQRT(d)\n ````\n ", + "languageDocumentationPopover.documentationESQL.st_contains": "ST_CONTAINS", + "languageDocumentationPopover.documentationESQL.st_contains.markdown": "\n\n ### ST_CONTAINS\n Renvoie si la première géométrie contient la deuxième géométrie.\n Il s'agit de l'inverse de la fonction `ST_WITHIN`.\n\n ````\n FROM airport_city_boundaries\n | WHERE ST_CONTAINS(city_boundary, TO_GEOSHAPE(\"POLYGON((109.35 18.3, 109.45 18.3, 109.45 18.4, 109.35 18.4, 109.35 18.3))\"))\n | KEEP abbrev, airport, region, city, city_location\n ````\n ", + "languageDocumentationPopover.documentationESQL.st_disjoint": "ST_DISJOINT", + "languageDocumentationPopover.documentationESQL.st_disjoint.markdown": "\n\n ### ST_DISJOINT\n Renvoie si les deux géométries ou colonnes géométriques sont disjointes.\n Il s'agit de l'inverse de la fonction `ST_INTERSECTS`.\n En termes mathématiques : ST_Disjoint(A, B) ⇔ A ⋂ B = ∅\n\n ````\n FROM airport_city_boundaries\n | WHERE ST_DISJOINT(city_boundary, TO_GEOSHAPE(\"POLYGON((-10 -60, 120 -60, 120 60, -10 60, -10 -60))\"))\n | KEEP abbrev, airport, region, city, city_location\n ````\n ", + "languageDocumentationPopover.documentationESQL.st_distance": "ST_DISTANCE", + "languageDocumentationPopover.documentationESQL.st_distance.markdown": "\n\n ### ST_DISTANCE\n Calcule la distance entre deux points.\n Pour les géométries cartésiennes, c’est la distance pythagoricienne dans les mêmes unités que les coordonnées d'origine.\n Pour les géométries géographiques, c’est la distance circulaire le long du grand cercle en mètres.\n\n ````\n Aéroports FROM\n | WHERE abbrev == \"CPH\"\n | EVAL distance = ST_DISTANCE(location, city_location)\n | KEEP abbrev, name, location, city_location, distance\n ````\n ", + "languageDocumentationPopover.documentationESQL.st_intersects": "ST_INTERSECTS", + "languageDocumentationPopover.documentationESQL.st_intersects.markdown": "\n\n ### ST_INTERSECTS\n Renvoie `true` (vrai) si deux géométries se croisent.\n Elles se croisent si elles ont un point commun, y compris leurs points intérieurs\n (les points situés le long des lignes ou dans des polygones).\n Il s'agit de l'inverse de la fonction `ST_DISJOINT`.\n En termes mathématiques : ST_Intersects(A, B) ⇔ A ⋂ B ≠ ∅\n\n ````\n Aéroports FROM\n | WHERE ST_INTERSECTS(location, TO_GEOSHAPE(\"POLYGON((42 14, 43 14, 43 15, 42 15, 42 14))\"))\n ````\n ", + "languageDocumentationPopover.documentationESQL.st_within": "ST_WITHIN", + "languageDocumentationPopover.documentationESQL.st_within.markdown": "\n\n ### ST_WITHIN\n Renvoie si la première géométrie est à l'intérieur de la deuxième géométrie.\n Il s'agit de l'inverse de la fonction `ST_CONTAINS`.\n\n ````\n FROM airport_city_boundaries\n | WHERE ST_WITHIN(city_boundary, TO_GEOSHAPE(\"POLYGON((109.1 18.15, 109.6 18.15, 109.6 18.65, 109.1 18.65, 109.1 18.15))\"))\n | KEEP abbrev, airport, region, city, city_location\n ````\n ", + "languageDocumentationPopover.documentationESQL.st_x": "ST_X", + "languageDocumentationPopover.documentationESQL.st_x.markdown": "\n\n ### ST_X\n Extrait la coordonnée `x` du point fourni.\n Si les points sont de type `geo_point`, cela revient à extraire la valeur de la `longitude`.\n\n ````\n ROW point = TO_GEOPOINT(\"POINT(42.97109629958868 14.7552534006536)\")\n | EVAL x = ST_X(point), y = ST_Y(point)\n ````\n ", + "languageDocumentationPopover.documentationESQL.st_y": "ST_Y", + "languageDocumentationPopover.documentationESQL.st_y.markdown": "\n\n ### ST_Y\n Extrait la coordonnée `y` du point fourni.\n Si les points sont de type `geo_point`, cela revient à extraire la valeur de la `latitude`.\n\n ````\n ROW point = TO_GEOPOINT(\"POINT(42.97109629958868 14.7552534006536)\")\n | EVAL x = ST_X(point), y = ST_Y(point)\n ````\n ", + "languageDocumentationPopover.documentationESQL.starts_with": "STARTS_WITH", + "languageDocumentationPopover.documentationESQL.starts_with.markdown": "\n\n ### STARTS_WITH\n Renvoie un booléen qui indique si une chaîne de mot-clés débute par une autre chaîne.\n\n ````\n FROM employees\n | KEEP last_name\n | EVAL ln_S = STARTS_WITH(last_name, \"B\")\n ````\n ", + "languageDocumentationPopover.documentationESQL.statsby": "STATS ... BY", + "languageDocumentationPopover.documentationESQL.statsby.markdown": "### STATS ... BY\nUtilisez `STATS ... BY` pour regrouper les lignes en fonction d'une valeur commune et calculer une ou plusieurs valeurs agrégées sur les lignes regroupées.\n\n**Exemples** :\n\n````\nFROM employees\n| STATS count = COUNT(emp_no) BY languages\n| SORT languages\n````\n\nSi `BY` est omis, le tableau de sortie contient exactement une ligne avec les agrégations appliquées sur l'ensemble des données :\n\n````\nFROM employees\n| STATS avg_lang = AVG(languages)\n````\n\nIl est possible de calculer plusieurs valeurs :\n\n````\nFROM employees\n| STATS avg_lang = AVG(languages), max_lang = MAX(languages)\n````\n\nIl est également possible d'effectuer des regroupements en fonction de plusieurs valeurs (uniquement pour les champs longs et les champs de la famille de mots-clés) :\n\n````\nFROM employees\n| EVAL hired = DATE_FORMAT(hire_date, \"YYYY\")\n| STATS avg_salary = AVG(salary) BY hired, languages.long\n| EVAL avg_salary = ROUND(avg_salary)\n| SORT hired, languages.long\n````\n\nConsultez la rubrique **Fonctions d'agrégation** pour obtenir la liste des fonctions pouvant être utilisées avec `STATS ... BY`.\n\nLes fonctions d'agrégation et les expressions de regroupement acceptent toutes deux d'autres fonctions. Ceci est utile pour utiliser `STATS...BY` sur des colonnes à valeur multiple. Par exemple, pour calculer l'évolution moyenne du salaire, vous pouvez utiliser `MV_AVG` pour faire la moyenne des multiples valeurs par employé, et utiliser le résultat avec la fonction `AVG` :\n\n````\nFROM employees\n| STATS avg_salary_change = AVG(MV_AVG(salary_change))\n````\n\nLe regroupement par expression est par exemple le regroupement des employés en fonction de la première lettre de leur nom de famille :\n\n````\nFROM employees\n| STATS my_count = COUNT() BY LEFT(last_name, 1)\n| SORT \"LEFT(last_name, 1)\"\n````\n\nIl n'est pas obligatoire d'indiquer le nom de la colonne de sortie. S'il n'est pas spécifié, le nouveau nom de la colonne est égal à l'expression. La requête suivante renvoie une colonne appelée `AVG(salary)` :\n\n````\nFROM employees\n| STATS AVG(salary)\n````\n\nComme ce nom contient des caractères spéciaux, il doit être placé entre deux caractères (`) lorsqu'il est utilisé dans des commandes suivantes :\n\n````\nFROM employees\n| STATS AVG(salary)\n| EVAL avg_salary_rounded = ROUND(\"AVG(salary)\")\n````\n\n**Remarque** : `STATS` sans aucun groupe est beaucoup plus rapide que l'ajout d'un groupe.\n\n**Remarque** : Le regroupement sur une seule expression est actuellement beaucoup plus optimisé que le regroupement sur plusieurs expressions.\n ", + "languageDocumentationPopover.documentationESQL.stringOperators": "LIKE et RLIKE", + "languageDocumentationPopover.documentationESQL.stringOperators.markdown": "### LIKE et RLIKE\nPour comparer des chaînes en utilisant des caractères génériques ou des expressions régulières, utilisez `LIKE` ou `RLIKE` :\n\nUtilisez `LIKE` pour faire correspondre des chaînes à l'aide de caractères génériques. Les caractères génériques suivants sont pris en charge :\n\n* `*` correspond à zéro caractère ou plus.\n* `?` correspond à un seul caractère.\n\n````\nFROM employees\n| WHERE first_name LIKE \"?b*\"\n| KEEP first_name, last_name\n````\n\nUtilisez `RLIKE` pour faire correspondre des chaînes à l'aide d'expressions régulières :\n\n````\nFROM employees\n| WHERE first_name RLIKE \".leja.*\"\n| KEEP first_name, last_name\n````\n ", + "languageDocumentationPopover.documentationESQL.substring": "SUBSTRING", + "languageDocumentationPopover.documentationESQL.substring.markdown": "\n\n ### SUBSTRING\n Renvoie la sous-chaîne d'une chaîne, délimitée en fonction d'une position de départ et d'une longueur facultative\n\n ````\n FROM employees\n | KEEP last_name\n | EVAL ln_sub = SUBSTRING(last_name, 1, 3)\n ````\n ", + "languageDocumentationPopover.documentationESQL.tan": "TAN", + "languageDocumentationPopover.documentationESQL.tan.markdown": "\n\n ### TAN\n Renvoie la fonction trigonométrique Tangente d'un angle.\n\n ````\n ROW a=1.8 \n | EVAL tan=TAN(a)\n ````\n ", + "languageDocumentationPopover.documentationESQL.tanh": "TANH", + "languageDocumentationPopover.documentationESQL.tanh.markdown": "\n\n ### TANH\n Renvoie la fonction hyperbolique Tangente d'un angle.\n\n ````\n ROW a=1.8 \n | EVAL tanh=TANH(a)\n ````\n ", + "languageDocumentationPopover.documentationESQL.tau": "TAU", + "languageDocumentationPopover.documentationESQL.tau.markdown": "\n\n ### TAU\n Renvoie le rapport entre la circonférence et le rayon d'un cercle.\n\n ````\n ROW TAU()\n ````\n ", + "languageDocumentationPopover.documentationESQL.to_base64": "TO_BASE64", + "languageDocumentationPopover.documentationESQL.to_base64.markdown": "\n\n ### TO_BASE64\n Encode une chaîne en chaîne base64.\n\n ````\n row a = \"elastic\" \n | eval e = to_base64(a)\n ````\n ", + "languageDocumentationPopover.documentationESQL.to_boolean": "TO_BOOLEAN", + "languageDocumentationPopover.documentationESQL.to_boolean.markdown": "\n\n ### TO_BOOLEAN\n Convertit une valeur d'entrée en une valeur booléenne.\n Une chaîne de valeur *true* sera convertie, sans tenir compte de la casse, en une valeur booléenne *true*.\n Pour toute autre valeur, y compris une chaîne vide, la fonction renverra *false*.\n La valeur numérique *0* sera convertie en *false*, toute autre valeur sera convertie en *true*.\n\n ````\n ROW str = [\"true\", \"TRuE\", \"false\", \"\", \"yes\", \"1\"]\n | EVAL bool = TO_BOOLEAN(str)\n ````\n ", + "languageDocumentationPopover.documentationESQL.to_cartesianpoint": "TO_CARTESIANPOINT", + "languageDocumentationPopover.documentationESQL.to_cartesianpoint.markdown": "\n\n ### TO_CARTESIANPOINT\n Convertit la valeur d'une entrée en une valeur `cartesian_point`.\n Une chaîne ne sera convertie que si elle respecte le format WKT Point.\n\n ````\n ROW wkt = [\"POINT(4297.11 -1475.53)\", \"POINT(7580.93 2272.77)\"]\n | MV_EXPAND wkt\n | EVAL pt = TO_CARTESIANPOINT(wkt)\n ````\n ", + "languageDocumentationPopover.documentationESQL.to_cartesianshape": "TO_CARTESIANSHAPE", + "languageDocumentationPopover.documentationESQL.to_cartesianshape.markdown": "\n\n ### TO_CARTESIANSHAPE\n Convertit une valeur d'entrée en une valeur `cartesian_shape`.\n Une chaîne ne sera convertie que si elle respecte le format WKT.\n\n ````\n ROW wkt = [\"POINT(4297.11 -1475.53)\", \"POLYGON ((3339584.72 1118889.97, 4452779.63 4865942.27, 2226389.81 4865942.27, 1113194.90 2273030.92, 3339584.72 1118889.97))\"]\n | MV_EXPAND wkt\n | EVAL geom = TO_CARTESIANSHAPE(wkt)\n ````\n ", + "languageDocumentationPopover.documentationESQL.to_datetime": "TO_DATETIME", + "languageDocumentationPopover.documentationESQL.to_datetime.markdown": "\n\n ### TO_DATETIME\n Convertit une valeur d'entrée en une valeur de date.\n Une chaîne ne sera convertie que si elle respecte le format `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'`.\n Pour convertir des dates vers d'autres formats, utilisez `DATE_PARSE`.\n\n ````\n ROW string = [\"1953-09-02T00:00:00.000Z\", \"1964-06-02T00:00:00.000Z\", \"1964-06-02 00:00:00\"]\n | EVAL datetime = TO_DATETIME(string)\n ````\n ", + "languageDocumentationPopover.documentationESQL.to_degrees": "TO_DEGREES", + "languageDocumentationPopover.documentationESQL.to_degrees.markdown": "\n\n ### TO_DEGREES\n Convertit un nombre en radians en degrés.\n\n ````\n ROW rad = [1.57, 3.14, 4.71]\n | EVAL deg = TO_DEGREES(rad)\n ````\n ", + "languageDocumentationPopover.documentationESQL.to_double": "TO_DOUBLE", + "languageDocumentationPopover.documentationESQL.to_double.markdown": "\n\n ### TO_DOUBLE\n Convertit une valeur d'entrée en une valeur double. Si le paramètre d'entrée est de type date,\n sa valeur sera interprétée en millisecondes depuis l'heure Unix,\n convertie en double. Le booléen *true* sera converti en double *1.0*, et *false* en *0.0*.\n\n ````\n ROW str1 = \"5.20128E11\", str2 = \"foo\"\n | EVAL dbl = TO_DOUBLE(\"520128000000\"), dbl1 = TO_DOUBLE(str1), dbl2 = TO_DOUBLE(str2)\n ````\n ", + "languageDocumentationPopover.documentationESQL.to_geopoint": "TO_GEOPOINT", + "languageDocumentationPopover.documentationESQL.to_geopoint.markdown": "\n\n ### TO_GEOPOINT\n Convertit une valeur d'entrée en une valeur `geo_point`.\n Une chaîne ne sera convertie que si elle respecte le format WKT Point.\n\n ````\n ROW wkt = \"POINT(42.97109630194 14.7552534413725)\"\n | EVAL pt = TO_GEOPOINT(wkt)\n ````\n ", + "languageDocumentationPopover.documentationESQL.to_geoshape": "TO_GEOSHAPE", + "languageDocumentationPopover.documentationESQL.to_geoshape.markdown": "\n\n ### TO_GEOSHAPE\n Convertit une valeur d'entrée en une valeur `geo_shape`.\n Une chaîne ne sera convertie que si elle respecte le format WKT.\n\n ````\n ROW wkt = \"POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))\"\n | EVAL geom = TO_GEOSHAPE(wkt)\n ````\n ", + "languageDocumentationPopover.documentationESQL.to_integer": "TO_INTEGER", + "languageDocumentationPopover.documentationESQL.to_integer.markdown": "\n\n ### TO_INTEGER\n Convertit une valeur d'entrée en une valeur entière.\n Si le paramètre d'entrée est de type date, sa valeur sera interprétée en millisecondes\n depuis l'heure Unix, convertie en entier.\n Le booléen *true* sera converti en entier *1*, et *false* en *0*.\n\n ````\n ROW long = [5013792, 2147483647, 501379200000]\n | EVAL int = TO_INTEGER(long)\n ````\n ", + "languageDocumentationPopover.documentationESQL.to_ip": "TO_IP", + "languageDocumentationPopover.documentationESQL.to_ip.markdown": "\n\n ### TO_IP\n Convertit une chaîne d'entrée en valeur IP.\n\n ````\n ROW str1 = \"1.1.1.1\", str2 = \"foo\"\n | EVAL ip1 = TO_IP(str1), ip2 = TO_IP(str2)\n | WHERE CIDR_MATCH(ip1, \"1.0.0.0/8\")\n ````\n ", + "languageDocumentationPopover.documentationESQL.to_long": "TO_LONG", + "languageDocumentationPopover.documentationESQL.to_long.markdown": "\n\n ### TO_LONG\n Convertit une valeur d'entrée en une valeur longue. Si le paramètre d'entrée est de type date,\n sa valeur sera interprétée en millisecondes depuis l'heure Unix, convertie en valeur longue.\n Le booléen *true* sera converti en valeur longue *1*, et *false* en *0*.\n\n ````\n ROW str1 = \"2147483648\", str2 = \"2147483648.2\", str3 = \"foo\"\n | EVAL long1 = TO_LONG(str1), long2 = TO_LONG(str2), long3 = TO_LONG(str3)\n ````\n ", + "languageDocumentationPopover.documentationESQL.to_lower": "TO_LOWER", + "languageDocumentationPopover.documentationESQL.to_lower.markdown": "\n\n ### TO_LOWER\n Renvoie une nouvelle chaîne représentant la chaîne d'entrée convertie en minuscules.\n\n ````\n ROW message = \"Some Text\"\n | EVAL message_lower = TO_LOWER(message)\n ````\n ", + "languageDocumentationPopover.documentationESQL.to_radians": "TO_RADIANS", + "languageDocumentationPopover.documentationESQL.to_radians.markdown": "\n\n ### TO_RADIANS\n Convertit un nombre en degrés en radians.\n\n ````\n ROW deg = [90.0, 180.0, 270.0]\n | EVAL rad = TO_RADIANS(deg)\n ````\n ", + "languageDocumentationPopover.documentationESQL.to_string": "TO_STRING", + "languageDocumentationPopover.documentationESQL.to_string.markdown": "\n\n ### TO_STRING\n Convertit une valeur d'entrée en une chaîne.\n\n ````\n ROW a=10\n | EVAL j = TO_STRING(a)\n ````\n ", + "languageDocumentationPopover.documentationESQL.to_unsigned_long": "TO_UNSIGNED_LONG", + "languageDocumentationPopover.documentationESQL.to_unsigned_long.markdown": "\n\n ### TO_UNSIGNED_LONG\n Convertit une valeur d'entrée en une valeur longue non signée. Si le paramètre d'entrée est de type date,\n sa valeur sera interprétée en millisecondes depuis l'heure Unix, convertie en valeur longue non signée.\n Le booléen *true* sera converti en valeur longue non signée *1*, et *false* en *0*.\n\n ````\n ROW str1 = \"2147483648\", str2 = \"2147483648.2\", str3 = \"foo\"\n | EVAL long1 = TO_UNSIGNED_LONG(str1), long2 = TO_ULONG(str2), long3 = TO_UL(str3)\n ````\n ", + "languageDocumentationPopover.documentationESQL.to_upper": "TO_UPPER", + "languageDocumentationPopover.documentationESQL.to_upper.markdown": "\n\n ### TO_UPPER\n Renvoie une nouvelle chaîne représentant la chaîne d'entrée convertie en majuscules.\n\n ````\n ROW message = \"Some Text\"\n | EVAL message_upper = TO_UPPER(message)\n ````\n ", + "languageDocumentationPopover.documentationESQL.to_version": "TO_VERSION", + "languageDocumentationPopover.documentationESQL.to_version.markdown": "\n\n ### TO_VERSION\n Convertit une chaîne d'entrée en une valeur de version.\n\n ````\n ROW v = TO_VERSION(\"1.2.3\")\n ````\n ", + "languageDocumentationPopover.documentationESQL.trim": "TRIM", + "languageDocumentationPopover.documentationESQL.trim.markdown": "\n\n ### TRIM\n Supprime les espaces de début et de fin d'une chaîne.\n\n ````\n ROW message = \" some text \", color = \" red \"\n | EVAL message = TRIM(message)\n | EVAL color = TRIM(color)\n ````\n ", + "languageDocumentationPopover.documentationESQL.where": "WHERE", + "languageDocumentationPopover.documentationESQL.where.markdown": "### WHERE\nUtilisez `WHERE` afin d'obtenir un tableau qui comprend toutes les lignes du tableau d'entrée pour lesquelles la condition fournie est évaluée à `true` :\n \n````\nFROM employees\n| KEEP first_name, last_name, still_hired\n| WHERE still_hired == true\n````\n\n#### Opérateurs\n\nPour obtenir un aperçu des opérateurs pris en charge, consultez la section **Opérateurs**.\n\n#### Fonctions\n`WHERE` prend en charge diverses fonctions de calcul des valeurs. Pour en savoir plus, consultez la section **Fonctions**.\n ", "textBasedEditor.query.textBasedLanguagesEditor.EnableWordWrapLabel": "Ajouter des sauts de ligne aux barres verticales", "textBasedEditor.query.textBasedLanguagesEditor.errorCount": "{count} {count, plural, one {erreur} other {erreurs}}", "textBasedEditor.query.textBasedLanguagesEditor.errorsTitle": "Erreurs", - "textBasedEditor.query.textBasedLanguagesEditor.esql": "ES|QL", "textBasedEditor.query.textBasedLanguagesEditor.expandLabel": "Développer", "textBasedEditor.query.textBasedLanguagesEditor.feedback": "Commentaires", - "textBasedEditor.query.textBasedLanguagesEditor.functions": "Fonctions", - "textBasedEditor.query.textBasedLanguagesEditor.functionsDocumentationESQLDescription": "Les fonctions sont compatibles avec \"ROW\" (Ligne), \"EVAL\" (Évaluation) et \"WHERE\" (Où).", - "textBasedEditor.query.textBasedLanguagesEditor.groupingFunctions": "Fonctions de groupage", - "textBasedEditor.query.textBasedLanguagesEditor.groupingFunctionsDocumentationESQLDescription": "Ces fonctions de regroupement peuvent être utilisées avec `STATS...BY` :", + "languageDocumentationPopover.documentationESQL.functions": "Fonctions", + "languageDocumentationPopover.documentationESQL.functionsDocumentationESQLDescription": "Les fonctions sont compatibles avec \"ROW\" (Ligne), \"EVAL\" (Évaluation) et \"WHERE\" (Où).", + "languageDocumentationPopover.documentationESQL.groupingFunctions": "Fonctions de groupage", + "languageDocumentationPopover.documentationESQL.groupingFunctionsDocumentationESQLDescription": "Ces fonctions de regroupement peuvent être utilisées avec `STATS...BY` :", "textBasedEditor.query.textBasedLanguagesEditor.hideQueriesLabel": "Masquer les recherches récentes", "textBasedEditor.query.textBasedLanguagesEditor.lineCount": "{count} {count, plural, one {ligne} other {lignes}}", "textBasedEditor.query.textBasedLanguagesEditor.lineNumber": "Ligne {lineNumber}", - "textBasedEditor.query.textBasedLanguagesEditor.operators": "Opérateurs", - "textBasedEditor.query.textBasedLanguagesEditor.operatorsDocumentationESQLDescription": "ES|QL est compatible avec les opérateurs suivants :", - "textBasedEditor.query.textBasedLanguagesEditor.processingCommands": "Traitement des commandes", - "textBasedEditor.query.textBasedLanguagesEditor.processingCommandsDescription": "Le traitement des commandes transforme un tableau des entrées par l'ajout, le retrait ou la modification des lignes et des colonnes. ES|QL est compatible avec le traitement des commandes suivant.", + "languageDocumentationPopover.documentationESQL.operators": "Opérateurs", + "languageDocumentationPopover.documentationESQL.operatorsDocumentationESQLDescription": "ES|QL est compatible avec les opérateurs suivants :", + "languageDocumentationPopover.documentationESQL.processingCommands": "Traitement des commandes", + "languageDocumentationPopover.documentationESQL.processingCommandsDescription": "Le traitement des commandes transforme un tableau des entrées par l'ajout, le retrait ou la modification des lignes et des colonnes. ES|QL est compatible avec le traitement des commandes suivant.", "textBasedEditor.query.textBasedLanguagesEditor.querieshistory.error": "La requête a échouée", "textBasedEditor.query.textBasedLanguagesEditor.querieshistory.success": "La requête a été exécuté avec succès", "textBasedEditor.query.textBasedLanguagesEditor.querieshistoryCopy": "Copier la requête dans le presse-papier", @@ -7341,7 +7339,7 @@ "textBasedEditor.query.textBasedLanguagesEditor.recentQueriesColumnLabel": "Recherches récentes", "textBasedEditor.query.textBasedLanguagesEditor.runQuery": "Exécuter la requête", "textBasedEditor.query.textBasedLanguagesEditor.showQueriesLabel": "Afficher les recherches récentes", - "textBasedEditor.query.textBasedLanguagesEditor.sourceCommands": "Commandes sources", + "languageDocumentationPopover.documentationESQL.sourceCommands": "Commandes sources", "textBasedEditor.query.textBasedLanguagesEditor.submitFeedback": "Soumettre un commentaire", "textBasedEditor.query.textBasedLanguagesEditor.timeRanColumnLabel": "Temps exécuté", "textBasedEditor.query.textBasedLanguagesEditor.timestampNotDetected": "@timestamp non trouvé", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f59908a1b5aa6..b82ff0f792701 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7077,255 +7077,253 @@ "telemetry.usageCollectionConstant": "使用状況の収集", "telemetry.usageDataTitle": "使用状況の収集", "textBasedEditor.query.textBasedLanguagesEditor.aborted": "リクエストが中断されました", - "textBasedEditor.query.textBasedLanguagesEditor.aggregationFunctions": "集約関数", - "textBasedEditor.query.textBasedLanguagesEditor.aggregationFunctionsDocumentationESQLDescription": "これらの関数はSTATS...BYで使用できます。", + "languageDocumentationPopover.documentationESQL.aggregationFunctions": "集約関数", + "languageDocumentationPopover.documentationESQL.aggregationFunctionsDocumentationESQLDescription": "これらの関数はSTATS...BYで使用できます。", "textBasedEditor.query.textBasedLanguagesEditor.cancel": "キャンセル", "textBasedEditor.query.textBasedLanguagesEditor.collapseLabel": "縮小", - "textBasedEditor.query.textBasedLanguagesEditor.commandsDescription": "通常、ソースコマンドはElasticsearchのデータを使ってテーブルを生成します。ES|QLは以下のソースコマンドをサポートしています。", + "languageDocumentationPopover.documentationESQL.commandsDescription": "通常、ソースコマンドはElasticsearchのデータを使ってテーブルを生成します。ES|QLは以下のソースコマンドをサポートしています。", "textBasedEditor.query.textBasedLanguagesEditor.disableWordWrapLabel": "パイプの改行を削除", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.abs": "ABS", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.abs.markdown": "\n\n ### ABS\n 絶対値を返します。\n\n ```\n ROW number = -1.0 \n | EVAL abs_number = ABS(number)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.acos": "ACOS", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.acos.markdown": "\n\n ### ACOS\n nのアークコサインをラジアンで表記された角度として返します。\n\n ```\n ROW a=.9\n | EVAL acos=ACOS(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.asin": "ASIN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.asin.markdown": "\n\n ### ASIN\n 入力\n 数値式のアークサインをラジアンで表記された角度として返します。\n\n ```\n ROW a=.9\n | EVAL asin=ASIN(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.atan": "ATAN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.atan.markdown": "\n\n ### ATAN\n 入力\n 数値式のアークタンジェントをラジアンで表記された角度として返します。\n\n ```\n ROW a=12.9\n | EVAL atan=ATAN(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.atan2": "ATAN2", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.atan2.markdown": "\n\n ### ATAN2\n 直交平面上の原点から点(x , y)に向かう光線と正のx軸のなす角(ラジアン表記)。\n \n\n ```\n ROW y=12.9, x=.6\n | EVAL atan2=ATAN2(y, x)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.autoBucketFunction": "BUCKET", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.autoBucketFunction.markdown": "### バケット\n日時または数値入力から、値(バケット)のグループを作成します。バケットのサイズは直接指定するか、推奨される数と値の範囲に基づいて選択できます。\n\nBUCKETは次の2つのモードで動作します。\n\n1.バケットのサイズがバケット数の提案(4つのパラメーター)と範囲に基づいて計算される。\n2.バケットサイズが直接指定される(2つのパラメーター)。\n\n目標バケット数、開始日、終了日を使用すると、目標バケット数以下のバケットを生成するために適切なバケットサイズがBUCKETによって選択されます。\n\nたとえば、1年に最大20バケットをリクエストすると、データが1か月間隔で整理されます。\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hire_date = MV_SORT(VALUES(hire_date)) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT hire_date\n```\n\n**注**:ここでは、正確な目標バケット数を指定するのではなく、目標バケット数を_上限_として範囲を指定します。\n\nBUCKETを集約と組み合わせ、ヒストグラムを作成できます。\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_month = COUNT(*) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT month\n```\n\n**注**:BUCKETは、どのドキュメントにも一致しないバケットを作成しません。そのため、前の例では1985-03-01やその他の日付が抜けています。\n\nその他のバケットを要求すると、範囲が小さくなることがあります。たとえば、1年に最大100バケットをリクエストすると、1週間単位のバケットになります。\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_week = COUNT(*) BY week = BUCKET(hire_date, 100, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT week\n```\n\n**注**:AUTO_BUCKETは行をフィルタリングしません。指定された範囲のみを使用して、適切なバケットサイズを選択します。範囲外の値の行に対しては、範囲外のバケツに対応するバケット値を返します。行をフィルタリングするには、BUCKETとWHEREを組み合わせます。\n\n事前に任意のバケットサイズがわかっている場合は、2番目の引数として指定し、範囲を除外します。\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_week = COUNT(*) BY week = BUCKET(hire_date, 1 week)\n| SORT week\n```\n\n**注**:バケットサイズを2番目のパラメーターとして指定するときには、時間の期間または日付の期間を選択する必要があります。\n\nBUCKETは数値フィールドでも動作します。たとえば、給与ヒストグラムを作成します。\n\n```\nFROM employees\n| STATS COUNT(*) by bs = BUCKET(salary, 20, 25324, 74999)\n| SORT bs\n```\n\n日付範囲で意図的フィルタリングする前の例とは異なり、数値フィールドでフィルタリングすることはほとんどありません。最小値と最大値を別々に見つける必要があります。ES|QLにはそれを自動的に実行するための簡単な方法がありません。\n\n任意のバケットサイズが事前にわかっている場合は、範囲を省略できます。2番目の引数として指定します。\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS c = COUNT(1) BY b = BUCKET(salary, 5000.)\n| SORT b\n```\n\n**注**:バケットサイズを2番目のパラメーターとして指定するときには、**浮動小数点数型**でなければなりません。\n\n次の例は、過去24時間の1時間単位のバケットを作成し、1時間当たりのイベント数を計算します。\n\n```\nFROM sample_data\n| WHERE @timestamp >= NOW() - 1 day and @timestamp < NOW()\n| STATS COUNT(*) BY bucket = BUCKET(@timestamp, 25, NOW() - 1 day, NOW())\n```\n\n次の例は、1985年の1か月単位のバケットを作成し、採用月別に平均給与を計算します。\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS AVG(salary) BY bucket = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT bucket\n```\n\n集約部で関数が**グループ部で定義されたエイリアスによって参照されている**場合、またはまったく同じ式で呼び出されている場合、BUCKETは、 STATS …​ BY …コマンドの集約部とグループ部の両方で使用できます。\n\n例:\n\n```\nFROM employees\n| STATS s1 = b1 + 1, s2 = BUCKET(salary / 1000 + 999, 50.) + 2 BY b1 = BUCKET(salary / 100 + 99, 50.), b2 = BUCKET(salary / 1000 + 999, 50.)\n| SORT b1, b2\n| KEEP s1, b1, s2, b2\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.binaryOperators": "バイナリ演算子", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.binaryOperators.markdown": "### バイナリ演算子\n次のバイナリ比較演算子がサポートされています。\n\n* 等号:`==`\n* 不等号:`!=`\n* より小さい:`<`\n* 以下:`<=`\n* より大きい:`>`\n* 以上:`>=`\n* 加算:`+`\n* 減算:`-`\n* 乗算:`*`\n* 除算:`/`\n* 係数:`%`\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.booleanOperators": "ブール演算子", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.booleanOperators.markdown": "### ブール演算子\n次のブール演算子がサポートされています。\n\n* `AND`\n* `OR`\n* `NOT`\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.bucket": "BUCKET", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.bucket.markdown": "\n\n ### BUCKET\n 日時または数値入力から、値(バケット)のグループを作成します。\n バケットのサイズは直接指定するか、推奨される数と値の範囲に基づいて選択できます。\n\n ```\n FROM employees\n | WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n | STATS hire_date = MV_SORT(VALUES(hire_date)) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n | SORT hire_date\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.case": "CASE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.case.markdown": "\n\n ### CASE\n 条件と値のペアを指定できます。この関数は、trueと評価される\n 最初の条件に属する値を返します。\n\n 引数の数が奇数の場合、最後の引数は条件に一致しない場合に返されるデフォルト値になります。\n 引数の数が偶数で、\n 条件が一致しない場合、この関数はnullを返します。\n\n ```\n FROM employees\n | EVAL type = CASE(\n languages <= 1, \"monolingual\",\n languages <= 2, \"bilingual\",\n \"polyglot\")\n | KEEP emp_no, languages, type\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.castOperator": "Cast (::)", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.castOperator.markdown": "### CAST (`::`)\n::演算子はO_型変換関数に代わる便利な構文です。\n\n例:\n```\nROW ver = CONCAT((\"0\"::INT + 1)::STRING, \".2.3\")::VERSION\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cbrt": "CBRT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cbrt.markdown": "\n\n ### CBRT\n 数値の立方根を返します。入力は任意の数値で、戻り値は常にdoubleです。\n 無限大の立方根はnullです。\n\n ```\n ROW d = 1000.0\n | EVAL c = cbrt(d)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ceil": "CEIL", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ceil.markdown": "\n\n ### CEIL\n 最も近い整数に数値を切り上げます。\n\n ```\n ROW a=1.8\n | EVAL a=CEIL(a)\n ```\n 注:これはlong(符号なしを含む)とintegerのnoopです。doubleの場合、JavaのMath.ceilと同様に、整数に最も近いdoubleの値を選びます。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cidr_match": "CIDR_MATCH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cidr_match.markdown": "\n\n ### CIDR_MATCH\n 指定されたIPが指定されたCIDRブロックのいずれかに含まれていればtrueを返します。\n\n ```\n FROM hosts \n | WHERE CIDR_MATCH(ip1, \"127.0.0.2/32\", \"127.0.0.3/32\") \n | KEEP card, host, ip0, ip1\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.coalesce": "COALESCE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.coalesce.markdown": "\n\n ### COALESCE\n nullでない最初の引数を返します。すべての引数がnullの場合はnullを返します。\n\n ```\n ROW a=null, b=\"b\"\n | EVAL COALESCE(a, b)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.concat": "CONCAT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.concat.markdown": "\n\n ### CONCAT\n 2つ以上の文字列を連結します。\n\n ```\n FROM employees\n | KEEP first_name, last_name\n | EVAL fullname = CONCAT(first_name, \" \", last_name)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cos": "COS", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cos.markdown": "\n\n ### COS\n 角度の余弦を返します。\n\n ```\n ROW a=1.8 \n | EVAL cos=COS(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cosh": "COSH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cosh.markdown": "\n\n ### COSH\n 角度の双曲余弦を返します。\n\n ```\n ROW a=1.8 \n | EVAL cosh=COSH(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_diff": "DATE_DIFF", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_diff.markdown": "\n\n ### DATE_DIFF\n startTimestampをendTimestampから減算し、unitの乗数の差を返します。\n startTimestampがendTimestampより後の場合は、負の値が返されます。\n\n ```\n ROW date1 = TO_DATETIME(\"2023-12-02T11:00:00.000Z\"), date2 = TO_DATETIME(\"2023-12-02T11:00:00.001Z\")\n | EVAL dd_ms = DATE_DIFF(\"microseconds\", date1, date2)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_extract": "DATE_EXTRACT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_extract.markdown": "\n\n ### DATE_EXTRACT\n 年、月、日、時間など、日付の一部を抽出します。\n\n ```\n ROW date = DATE_PARSE(\"yyyy-MM-dd\", \"2022-05-06\")\n | EVAL year = DATE_EXTRACT(\"year\", date)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_format": "DATE_FORMAT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_format.markdown": "\n\n ### DATE_FORMAT\n 指定した書式の日付の文字列表現を返します。\n\n ```\n FROM employees\n | KEEP first_name, last_name, hire_date\n | EVAL hired = DATE_FORMAT(\"YYYY-MM-dd\", hire_date)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_parse": "DATE_PARSE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_parse.markdown": "\n\n ### DATE_PARSE\n 最初の引数で指定した形式を使用して、2番目の引数を解析することで、日付を返します。\n\n ```\n ROW date_string = \"2022-05-06\"\n | EVAL date = DATE_PARSE(\"yyyy-MM-dd\", date_string)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_trunc": "DATE_TRUNC", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_trunc.markdown": "\n\n ### DATE_TRUNC\n 最も近い区間まで日付を切り捨てます。\n\n ```\n FROM employees\n | KEEP first_name, last_name, hire_date\n | EVAL year_hired = DATE_TRUNC(1 year, hire_date)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.dissect": "DISSECT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.dissect.markdown": "### DISSECT\nDISSECTは文字列から構造化データを取り出すことができます。DISSECTは文字列を区切り文字ベースのパターンと照合し、指定されたキーを列として抽出します。\n\ndissectパターンの構文については、[dissectプロセッサードキュメント](https://www.elastic.co/guide/en/elasticsearch/reference/current/dissect-processor.html)を参照してください。\n\n```\nROW a = \"1953-01-23T12:15:00Z - some text - 127.0.0.1\"\n| DISSECT a \"%'{Y}-%{M}-%{D}T%{h}:%{m}:%{s}Z - %{msg} - %{ip}'\"\n``` ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.drop": "DROP", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.drop.markdown": "### DROP\nテーブルから列を削除するには、DROPを使用します。\n \n```\nFROM employees\n| DROP height\n```\n\n各列を名前で指定するのではなく、ワイルドカードを使って、パターンと一致する名前の列をすべて削除することができます。\n\n```\nFROM employees\n| DROP height*\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.e": "E", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.e.markdown": "\n\n ### E\n オイラー数を返します。\n\n ```\n ROW E()\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ends_with": "ENDS_WITH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ends_with.markdown": "\n\n ### ENDS_WITH\n キーワード文字列が他の文字列で終わるかどうかを示すブール値を返します。\n\n ```\n FROM employees\n | KEEP last_name\n | EVAL ln_E = ENDS_WITH(last_name, \"d\")\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.enrich": "ENRICH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.enrich.markdown": "### ENRICH\nENRICH`を使用すると、既存のインデックスのデータを受信レコードに追加することができます。[インジェストエンリッチ](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html)と似ていますが、クエリー時に動作します。\n\n```\nROW language_code = \"1\"\n| ENRICH languages_policy\n```\n\nENRICHでは、[エンリッチポリシー](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html#enrich-policy)を実行する必要があります。エンリッチポリシーは、一致フィールド (キーフィールド) とエンリッチフィールドのセットを定義します。\n\nENRICHは、一致フィールド値に基づいて、[エンリッチインデックス](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html#enrich-index)のレコードを検索します。入力データセットの一致するキーは、ON を使用して定義できます。指定しない場合は、エンリッチポリシーで定義された一致フィールドと同じ名前のフィールドで一致が実行されます。\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a\n```\n\nWITH , ...構文を使用して、結果に追加される属性(ポリシーでエンリッチフィールドとして定義された属性の間)を指定できます。\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a WITH language_name\n```\n\n属性の名前は、WITH new_name=を使用して変更できます。\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a WITH name = language_name\n```\n\nデフォルトでは、(WITHが定義されていない場合)、ENRICHはエンリッチポリシーで定義されたすべてのエンリッチフィールドを結果に追加します。\n\n名前の競合が発生した場合、新しく作成されたフィールドが既存のフィールドを上書きします。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.eval": "EVAL", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.eval.markdown": "### EVAL\nEVALでは、新しい列を追加できます。\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| EVAL height_feet = height * 3.281, height_cm = height * 100\n```\n\n指定した列がすでに存在する場合、既存の列は削除され、新しい列がテーブルに追加されます。\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| EVAL height = height * 3.281\n```\n\n#### 関数\nEVALは値を計算するためのさまざまな関数をサポートしています。関数をクリックすると詳細が表示されます。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.floor": "FLOOR", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.floor.markdown": "\n\n ### FLOOR\n 最も近い整数に数値を切り捨てます。\n\n ```\n ROW a=1.8\n | EVAL a=FLOOR(a)\n ```\n 注:これはlong(符号なしを含む)とintegerのnoopです。\n doubleの場合、Math.floorと同様に、整数に最も近いdoubleの値を選びます。\n \n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.from": "FROM", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.from_base64": "FROM_BASE64", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.from_base64.markdown": "\n\n ### FROM_BASE64\n base64文字列をデコードします。\n\n ```\n row a = \"ZWxhc3RpYw==\" \n | eval d = from_base64(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.from.markdown": "### FROM\nソースコマンドFROMは、データストリーム、インデックス、またはエイリアスから、最大10,000ドキュメントを含むテーブルを返します。結果のテーブルの各行はドキュメントを表します。各列はフィールドに対応し、そのフィールドの名前でアクセスできます。\n\n```\nFROM employees\n```\n\n[日付演算](https://www.elastic.co/guide/en/elasticsearch/reference/current/api-conventions.html#api-date-math-index-names) を使用して、インデックス、エイリアス、データストリームを参照できます。これは時系列データの場合に便利です。\n\nカンマ区切りのリストまたはワイルドカードを使用して、複数のデータストリーム、インデックス、またはエイリアスをクエリーします。\n\n```\nFROM employees-00001,employees-*\n```\n\n#### メタデータ\n\nES|QLは以下のメタデータフィールドにアクセスできます。\n\n* `_index`:ドキュメントが属するインデックス。このフィールドはkeyword型です。\n* `_id`:ソースドキュメントのID。このフィールドはkeyword型です。### `_version`:ソースドキュメントのバージョン。フィールドの型はlongです。\n\nメタデータフィールドを有効にするには、METADATAディレクティブを使います。\n\n```\nFROM index [METADATA _index, _id]\n```\n\nメタデータフィールドは、データのソースがインデックスである場合にのみ使用できます。その結果、FROMはMETADATAディレクティブをサポートする唯一のソースコマンドです。\n\nこのフィールドが有効になると、他のインデックスフィールドと同様に、後続の処理コマンドで利用できるようになります。\n\n```\nFROM ul_logs, apps [METADATA _index, _version]\n| WHERE id IN (13, 14) AND _version == 1\n| EVAL key = CONCAT(_index, \"_\", TO_STR(id))\n| SORT id, _index\n| KEEP id, _index, _version, key\n```\n\nまた、インデックス・フィールドと同様に、一度集約が実行されると、グループ化フィールドとして使用されないかぎり、メタデータフィールドは後続のコマンドからはアクセスできなくなります。\n\n```\nFROM employees [METADATA _index, _id]\n| STATS max = MAX(emp_no) BY _index\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.greatest": "GREATEST", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.greatest.markdown": "\n\n ### GREATEST\n 多数の列から最大値を返します。これはMV_MAX\n と似ていますが、一度に複数の列に対して実行します。\n\n ```\n ROW a = 10, b = 20\n | EVAL g = GREATEST(a, b)\n ```\n 注:keywordまたはtextフィールドに対して実行すると、アルファベット順の最後の文字列を返します。boolean列に対して実行すると、値がtrueの場合にtrueを返します。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.grok": "GROK", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.grok.markdown": "### GROK\nGROKを使うと、文字列から構造化データを抽出できます。GROKは正規表現に基づいて文字列をパターンと一致させ、指定されたパターンを列として抽出します。\n\ngrokパターンの構文については、 [grokプロセッサードキュメント](https://www.elastic.co/guide/en/elasticsearch/reference/current/grok-processor.html)を参照してください。\n\n```\nROW a = \"12 15.5 15.6 true\"\n| GROK a \"%'{NUMBER:b:int}' %'{NUMBER:c:float}' %'{NUMBER:d:double}' %'{WORD:e:boolean}'\"\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.inOperator": "IN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.inOperator.markdown": "### IN\nIN演算子は、フィールドや式がリテラル、フィールド、式のリストの要素と等しいかどうかをテストすることができます。\n\n```\nROW a = 1, b = 4, c = 3\n| WHERE c-a IN (3, b / 2, a)\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ip_prefix": "IP_PREFIX", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ip_prefix.markdown": "\n\n ### IP_PREFIX\n IPを特定のプレフィックス長に切り詰めます。\n\n ```\n row ip4 = to_ip(\"1.2.3.4\"), ip6 = to_ip(\"fe80::cae2:65ff:fece:feb9\")\n | eval ip4_prefix = ip_prefix(ip4, 24, 0), ip6_prefix = ip_prefix(ip6, 0, 112);\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.keep": "KEEP", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.keep.markdown": "### KEEP\nKEEPコマンドは、返される列と、列が返される順序を指定することができます。\n\n返される列を制限するには、カンマで区切りの列名リストを使用します。列は指定された順序で返されます。\n \n```\nFROM employees\n| KEEP first_name, last_name, height\n```\n\n各列を名前で指定するのではなく、ワイルドカードを使って、パターンと一致する名前の列をすべて返すことができます。\n\n```\nFROM employees\n| KEEP h*\n```\n\nアスタリスクワイルドカード(*)は単独で、他の引数と一致しないすべての列に変換されます。このクエリーは、最初にhで始まる名前の列をすべて返し、その後にその他の列をすべて返します。\n\n```\nFROM employees\n| KEEP h*, *\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.least": "LEAST", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.least.markdown": "\n\n ### LEAST\n 多数の列から最小値を返します。これはMV_MINと似ていますが、一度に複数の列に対して実行します。\n\n ```\n ROW a = 10, b = 20\n | EVAL l = LEAST(a, b)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.left": "LEFT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.left.markdown": "\n\n ### LEFT\n stringから左から順にlength文字を抜き出したサブ文字列を返します。\n\n ```\n FROM employees\n | KEEP last_name\n | EVAL left = LEFT(last_name, 3)\n | SORT last_name ASC\n | LIMIT 5\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.length": "LENGTH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.length.markdown": "\n\n ### LENGTH\n 文字列の文字数を返します。\n\n ```\n FROM employees\n | KEEP first_name, last_name\n | EVAL fn_length = LENGTH(first_name)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.limit": "LIMIT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.limit.markdown": "### LIMIT\nLIMIT`処理コマンドは行数を制限することができます。\n \n```\nFROM employees\n| LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.locate": "LOCATE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.locate.markdown": "\n\n ### LOCATE\n 別の文字列内のキーワードサブ文字列の位置を示す整数を返します。\n\n ```\n row a = \"hello\"\n | eval a_ll = locate(a, \"ll\")\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.log": "LOG", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.log.markdown": "\n\n ### LOG\n 基数に対する値の対数を返します。入力は任意の数値で、戻り値は常にdoubleです。\n\n ゼロの対数、負数、1の基数はnullと警告を返します。\n\n ```\n ROW base = 2.0, value = 8.0\n | EVAL s = LOG(base, value)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.log10": "LOG10", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.log10.markdown": "\n\n ### LOG10\n 基数10に対する値の対数を返します。入力は任意の数値で、戻り値は常にdoubleです。\n\n 0の対数および負数はnullと警告を返します。\n\n ```\n ROW d = 1000.0 \n | EVAL s = LOG10(d)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ltrim": "LTRIM", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ltrim.markdown": "\n\n ### LTRIM\n 文字列から先頭の空白を取り除きます。\n\n ```\n ROW message = \" some text \", color = \" red \"\n | EVAL message = LTRIM(message)\n | EVAL color = LTRIM(color)\n | EVAL message = CONCAT(\"'\", message, \"'\")\n | EVAL color = CONCAT(\"'\", color, \"'\")\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.markdown": "## ES|QL\n\nES|QL(Elasticsearch クエリー言語)クエリーは、パイプ文字の|で区切られた一連のコマンドで構成されます。各クエリーは**ソースコマンド**で始まり、通常はElasticsearchのデータを使ってテーブルを生成します。\n\nソースコマンドには、1つ以上の**処理コマンド**を続けることができます。処理コマンドは、行や列を追加、削除、変更することで、前のコマンドの出力テーブルを変更することができます。\n\n```\nsource-command\n| processing-command1\n| processing-command2\n```\n\nクエリーの結果は、最終的な処理コマンドによって生成されるテーブルです。 \n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_append": "MV_APPEND", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_append.markdown": "\n\n ### MV_APPEND\n 2つの複数値フィールドの値を連結します\n\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_avg": "MV_AVG", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_avg.markdown": "\n\n ### MV_AVG\n 複数値フィールドを、すべての値の平均を含む単一値フィールドに変換します。\n\n ```\n ROW a=[3, 5, 1, 6]\n | EVAL avg_a = MV_AVG(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_concat": "MV_CONCAT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_concat.markdown": "\n\n ### MV_CONCAT\n 複数値文字列式を、区切り文字で区切られたすべての値を連結した単一値列に変換します。\n\n ```\n ROW a=[\"foo\", \"zoo\", \"bar\"]\n | EVAL j = MV_CONCAT(a, \", \")\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_count": "MV_COUNT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_count.markdown": "\n\n ### MV_COUNT\n 複数値式を、値の数をカウントする単一値列に変換します。\n\n ```\n ROW a=[\"foo\", \"zoo\", \"bar\"]\n | EVAL count_a = MV_COUNT(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_dedupe": "MV_DEDUPE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_dedupe.markdown": "\n\n ### MV_DEDUPE\n 複数値フィールドから重複する値を削除します。\n\n ```\n ROW a=[\"foo\", \"foo\", \"bar\", \"foo\"]\n | EVAL dedupe_a = MV_DEDUPE(a)\n ```\n 注:MV_DEDUPEは列の値をソートすることがありますが、常にソートするわけではありません。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_first": "MV_FIRST", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_first.markdown": "\n\n ### MV_FIRST\n \n 複数値式を、最初の値を含む単一値列に変換します。これは、SPLITなどの既知の順序で複数値列を発行する関数から読み取るときに役立ちます。\n \n\n 複数値フィールドが基本ストレージから読み取られる順序は保証されません。\n \n 通常は昇順ですが、必ずしもそうであるわけではありません。最小値が必要な場合は、MV_FIRSTの代わりに、MV_MINを使用します。\n MV_MINは、ソートされた値向けに最適化されているため、\n MV_FIRSTにパフォーマンスの利点はありません。\n\n ```\n ROW a=\"foo;bar;baz\"\n | EVAL first_a = MV_FIRST(SPLIT(a, \";\"))\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_last": "MV_LAST", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_last.markdown": "\n\n ### MV_LAST\n \n 複数値式を、最後の値を含む単一値列に変換します。これは、SPLITなどの既知の順序で複数値列を発行する関数から読み取るときに役立ちます。\n \n\n 複数値フィールドが基本ストレージから読み取られる順序は保証されません。\n \n 通常は昇順ですが、必ずしもそうであるわけではありません。最大値が必要な場合は、MV_LASTの代わりに、MV_MAXを使用します。\n MV_MAXは、ソートされた値向けに最適化されているため、\n MV_LASTにパフォーマンスの利点はありません。\n\n ```\n ROW a=\"foo;bar;baz\"\n | EVAL last_a = MV_LAST(SPLIT(a, \";\"))\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_max": "MV_MAX", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_max.markdown": "\n\n ### MV_MAX\n 複数値フィールドを、最大値を含む単一値フィールドに変換します。\n\n ```\n ROW a=[3, 5, 1]\n | EVAL max_a = MV_MAX(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_median": "MV_MEDIAN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_median.markdown": "\n\n ### MV_MEDIAN\n 複数値フィールドを、中央値を含む単一値フィールドに変換します。\n\n ```\n ROW a=[3, 5, 1]\n | EVAL median_a = MV_MEDIAN(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_min": "MV_MIN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_min.markdown": "\n\n ### MV_MIN\n 複数値フィールドを、最小値を含む単一値フィールドに変換します。\n\n ```\n ROW a=[2, 1]\n | EVAL min_a = MV_MIN(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_slice": "MV_SLICE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_slice.markdown": "\n\n ### MV_SLICE\n 開始インデックス値と終了インデックス値を使用して、複数値フィールドのサブセットを返します。\n\n ```\n row a = [1, 2, 2, 3]\n | eval a1 = mv_slice(a, 1), a2 = mv_slice(a, 2, 3)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_sort": "MV_SORT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_sort.markdown": "\n\n ### MV_SORT\n 辞書の順序で複数値フィールドを並べ替えます。\n\n ```\n ROW a = [4, 2, -3, 2]\n | EVAL sa = mv_sort(a), sd = mv_sort(a, \"DESC\")\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_sum": "MV_SUM", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_sum.markdown": "\n\n ### MV_SUM\n 複数値フィールドを、すべての値の合計を含む単一値フィールドに変換します。\n\n ```\n ROW a=[3, 5, 6]\n | EVAL sum_a = MV_SUM(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_zip": "MV_ZIP", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_zip.markdown": "\n\n ### MV_ZIP\n 値を結合する区切り文字を使用して、2つの複数値フィールドの値を結合します。\n\n ```\n ROW a = [\"x\", \"y\", \"z\"], b = [\"1\", \"2\"]\n | EVAL c = mv_zip(a, b, \"-\")\n | KEEP a, b, c\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mvExpand": "MV_EXPAND", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mvExpand.markdown": "### MV_EXPAND\nMV_EXPAND処理コマンドは、複数値フィールドを値ごとに1行に展開し、他のフィールドを複製します。 \n```\nROW a=[1,2,3], b=\"b\", j=[\"a\",\"b\"]\n| MV_EXPAND a\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.now": "NOW", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.now.markdown": "\n\n ### NOW\n 現在の日付と時刻を返します。\n\n ```\n ROW current_date = NOW()\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.pi": "PI", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.pi.markdown": "\n\n ### PI\n 円の円周と直径の比率であるPiを返します。\n\n ```\n ROW PI()\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.pow": "POW", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.pow.markdown": "\n\n ### POW\n exponentのべき乗にしたbaseの値を返します。\n\n ```\n ROW base = 2.0, exponent = 2\n | EVAL result = POW(base, exponent)\n ```\n 注:ここでは、倍精度浮動小数点数の結果でもオーバーフローする可能性があります。その場合は、NULLが返されます。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.predicates": "NULL値", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.predicates.markdown": "### NULL値\nNULLの比較には、IS NULLとIS NOT NULL述語を使います。\n\n```\nFROM employees\n| WHERE birth_date IS NULL\n| KEEP first_name, last_name\n| SORT first_name\n| LIMIT 3\n```\n\n```\nFROM employees\n| WHERE is_rehired IS NOT NULL\n| STATS count(emp_no)\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.rename": "RENAME", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.rename.markdown": "### RENAME\nRENAMEを使用して、次の構文で列の名前を変更します。\n\n```\nRENAME AS \n```\n\n例:\n\n```\nFROM employees\n| KEEP first_name, last_name, still_hired\n| RENAME still_hired AS employed\n```\n\n新しい名前の列がすでに存在する場合、その列は新しい列に置き換えられます。\n\n複数の列の名前を1つのRENAMEコマンドで変更することができます。\n\n```\nFROM employees\n| KEEP first_name, last_name\n| RENAME first_name AS fn, last_name AS ln\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.repeat": "REPEAT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.repeat.markdown": "\n\n ### REPEAT\n 指定したnumberの回数、文字列stringとそれ自身を連結して構成された文字列を返します。\n\n ```\n ROW a = \"Hello!\"\n | EVAL triple_a = REPEAT(a, 3);\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.replace": "REPLACE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.replace.markdown": "\n\n ### REPLACE\n \n この関数は、正規表現regexと置換文字列newStrの任意の一致を文字列strに代入します。\n\n ```\n ROW str = \"Hello World\"\n | EVAL str = REPLACE(str, \"World\", \"Universe\")\n | KEEP str\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.right": "RIGHT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.right.markdown": "\n\n ### RIGHT\n strのうち右から数えてlength文字までのサブ文字列を返します。\n\n ```\n FROM employees\n | KEEP last_name\n | EVAL right = RIGHT(last_name, 3)\n | SORT last_name ASC\n | LIMIT 5\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.round": "ROUND", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.round.markdown": "\n\n ### ROUND\n 数値を指定した小数点以下の桁数に丸めます。\n デフォルトは0で、最も近い整数を返します。\n 精度が負の場合、小数点以下の桁数に丸めます。\n \n\n ```\n FROM employees\n | KEEP first_name, last_name, height\n | EVAL height_ft = ROUND(height * 3.281, 1)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.row": "ROW", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.row.markdown": "### ROW\nROWソースコマンドは、指定した値の列を1つ以上含む行を作成します。これはテストの場合に便利です。\n \n```\nROW a = 1, b = \"two\", c = null\n```\n\n複数の値を含む列を作成するには角括弧を使用します。\n\n```\nROW a = [2, 1]\n```\n\nROWは関数の使用をサポートしています。\n\n```\nROW a = ROUND(1.23, 0)\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.rtrim": "RTRIM", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.rtrim.markdown": "\n\n ### RTRIM\n 文字列から末尾の空白を取り除きます。\n\n ```\n ROW message = \" some text \", color = \" red \"\n | EVAL message = RTRIM(message)\n | EVAL color = RTRIM(color)\n | EVAL message = CONCAT(\"'\", message, \"'\")\n | EVAL color = CONCAT(\"'\", color, \"'\")\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.show": "SHOW", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.show.markdown": "### SHOW\nSHOW ソースコマンドはデプロイとその能力に関する情報を返します。\n\n* デプロイのバージョン、ビルド日、ハッシュを返すには、SHOW INFOを使用します。\n* SHOW FUNCTIONSを使用すると、サポートされているすべての関数のリストと各関数の概要を返します。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.signum": "SIGNUM", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.signum.markdown": "\n\n ### SIGNUM\n 任意の数値の符号を返します。\n 負の数値の場合は-1を返します。0の場合は0を返します。正の数値の場合は1を返します。\n\n ```\n ROW d = 100.0\n | EVAL s = SIGNUM(d)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sin": "SIN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sin.markdown": "\n\n ### SIN\n 角度の正弦三角関数を返します。\n\n ```\n ROW a=1.8 \n | EVAL sin=SIN(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sinh": "SINH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sinh.markdown": "\n\n ### SINH\n 角度の双曲線正弦を返します。\n\n ```\n ROW a=1.8 \n | EVAL sinh=SINH(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sort": "SORT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sort.markdown": "### SORT\nSORTコマンドを使用すると、1つ以上のフィールドで行をソートすることができます。\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height\n```\n\nデフォルトのソート順は昇順です。ASCまたはDESCを使って明示的なソート順を設定します。\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height DESC\n```\n\n2つの行のソートキーが同じ場合、元の順序が保持されます。タイブレーカーとなるソート式を追加で指定できます。\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height DESC, first_name ASC\n```\n\n#### null値\nデフォルトでは、null値は他のどの値よりも大きい値として扱われます。昇順のソートではnull値は最後にソートされ、降順のソートではnull値は最初にソートされます。NULLS FIRSTまたはNULLS LASTを指定することで変更できます。\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| SORT first_name ASC NULLS FIRST\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.split": "SPLIT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.split.markdown": "\n\n ### SPLIT\n 単一の値の文字列を複数の文字列に分割します。\n\n ```\n ROW words=\"foo;bar;baz;qux;quux;corge\"\n | EVAL word = SPLIT(words, \";\")\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sqrt": "SQRT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sqrt.markdown": "\n\n ### SQRT\n 数値の平方根を返します。入力は任意の数値で、戻り値は常にdoubleです。\n 負数と無限大の平方根はnullです。\n\n ```\n ROW d = 100.0\n | EVAL s = SQRT(d)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_contains": "ST_CONTAINS", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_contains.markdown": "\n\n ### ST_CONTAINS\n 最初のジオメトリに2番目のジオメトリが含まれるかどうかを返します。\n これはST_WITHIN関数の逆関数です。\n\n ```\n FROM airport_city_boundaries\n | WHERE ST_CONTAINS(city_boundary, TO_GEOSHAPE(\"POLYGON((109.35 18.3, 109.45 18.3, 109.45 18.4, 109.35 18.4, 109.35 18.3))\"))\n | KEEP abbrev, airport, region, city, city_location\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_disjoint": "ST_DISJOINT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_disjoint.markdown": "\n\n ### ST_DISJOINT\n 2つのジオメトリまたはジオメトリ列が結合解除されているかどうかを返します。\n これはST_INTERSECTS関数の逆関数です。\n 数学的には次のようになります。ST_Disjoint(A, B) ⇔ A ⋂ B = ∅\n\n ```\n FROM airport_city_boundaries\n | WHERE ST_DISJOINT(city_boundary, TO_GEOSHAPE(\"POLYGON((-10 -60, 120 -60, 120 60, -10 60, -10 -60))\"))\n | KEEP abbrev, airport, region, city, city_location\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_distance": "ST_DISTANCE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_distance.markdown": "\n\n ### ST_DISTANCE\n 2点間の距離を計算します。\n デカルト幾何学の場合、これは元の座標と同じ単位でのピタゴラス距離です。\n 地理的幾何学では、これはメートル単位での円に沿った円周距離です。\n\n ```\n FROM airports\n | WHERE abbrev == \"CPH\"\n | EVAL distance = ST_DISTANCE(location, city_location)\n | KEEP abbrev, name, location, city_location, distance\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_intersects": "ST_INTERSECTS", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_intersects.markdown": "\n\n ### ST_INTERSECTS\n 2つのジオメトリが交差している場合はTrueを返します。\n 内部点を含め、共通の点がある場合は交差しています\n (線に沿った点または多角形内の点)。\n これはST_DISJOINT関数の逆関数です。\n 数学的には次のようになります。ST_Intersects(A, B) ⇔ A ⋂ B ≠ ∅\n\n ```\n FROM airports\n | WHERE ST_INTERSECTS(location, TO_GEOSHAPE(\"POLYGON((42 14, 43 14, 43 15, 42 15, 42 14))\"))\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_within": "ST_WITHIN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_within.markdown": "\n\n ### ST_WITHIN\n 最初のジオメトリが2番目のジオメトリ内にあるかどうかを返します。\n これはST_CONTAINS関数の逆関数です。\n\n ```\n FROM airport_city_boundaries\n | WHERE ST_WITHIN(city_boundary, TO_GEOSHAPE(\"POLYGON((109.1 18.15, 109.6 18.15, 109.6 18.65, 109.1 18.65, 109.1 18.15))\"))\n | KEEP abbrev, airport, region, city, city_location\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_x": "ST_X", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_x.markdown": "\n\n ### ST_X\n 指定された点からx座標を抽出します。\n この点がgeo_pointタイプの場合は、longitude値を抽出するのと同じ結果になります。\n\n ```\n ROW point = TO_GEOPOINT(\"POINT(42.97109629958868 14.7552534006536)\")\n | EVAL x = ST_X(point), y = ST_Y(point)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_y": "ST_Y", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_y.markdown": "\n\n ### ST_Y\n 指定された点からy座標を抽出します。\n この点がgeo_pointタイプの場合は、latitude値を抽出するのと同じ結果になります。\n\n ```\n ROW point = TO_GEOPOINT(\"POINT(42.97109629958868 14.7552534006536)\")\n | EVAL x = ST_X(point), y = ST_Y(point)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.starts_with": "STARTS_WITH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.starts_with.markdown": "\n\n ### STARTS_WITH\n キーワード文字列が他の文字列で始まるかどうかを示すブール値を返します。\n\n ```\n FROM employees\n | KEEP last_name\n | EVAL ln_S = STARTS_WITH(last_name, \"B\")\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.statsby": "STATS ...BY", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.statsby.markdown": "### STATS ...BY\nSTATS ...BYを使用すると、共通の値に従って行をグループ化し、グループ化された行に対する1つ以上の集約値を計算します。\n\n**例**:\n\n```\nFROM employees\n| STATS count = COUNT(emp_no) BY languages\n| SORT languages\n```\n\nBYが省略された場合、出力テーブルには、データセット全体に適用された集約が正確に1行だけ含まれます。\n\n```\nFROM employees\n| STATS avg_lang = AVG(languages)\n```\n\n複数の値を計算することができます。\n\n```\nFROM employees\n| STATS avg_lang = AVG(languages), max_lang = MAX(languages)\n```\n\n複数の値でグループ化することも可能です(longおよびkeywordファミリーフィールドでのみサポート)。\n\n```\nFROM employees\n| EVAL hired = DATE_FORMAT(hire_date, \"YYYY\")\n| STATS avg_salary = AVG(salary) BY hired, languages.long\n| EVAL avg_salary = ROUND(avg_salary)\n| SORT hired, languages.long\n```\n\nSTATS ...BYで使用できる関数の一覧については、**集計関数**を参照してください。\n\n集計関数とグループ式の両方で他の関数を使用できます。これは、複数値列でSTATS...BYを使用するときに有用です。たとえば、平均給与変動を計算するには、まず、MV_AVGを使用して従業員ごとに複数の値の平均を求め、その結果にAVG関数を適用します。\n\n```\nFROM employees\n| STATS avg_salary_change = AVG(MV_AVG(salary_change))\n```\n\n式によるグループ化の例は、姓の最初の文字で従業員をグループ化することです。\n\n```\nFROM employees\n| STATS my_count = COUNT() BY LEFT(last_name, 1)\n| SORT `LEFT(last_name, 1)`\n```\n\n出力列名の指定は任意です。指定しない場合は、新しい列名が式と等しくなります。次のクエリーは列\"AVG(salary)\"を返します。\n\n```\nFROM employees\n| STATS AVG(salary)\n```\n\nこの名前には特殊文字が含まれているため、後続のコマンドで使用するときには、バッククオート(`)で囲む必要があります。\n\n```\nFROM employees\n| STATS AVG(salary)\n| EVAL avg_salary_rounded = ROUND(`AVG(salary)`)\n```\n\n**注**:グループなしのSTATSは、グループを追加するよりも大幅に高速です。\n\n**注**:単一式でのグループは、現在、複数式でのグループよりも大幅に最適化されています。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.stringOperators": "LIKEおよびRLIKE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.stringOperators.markdown": "### LIKEおよびRLIKE\nワイルドカードや正規表現を使った文字列比較にはLIKEまたはRLIKEを使います。\n\nワイルドカードを使って文字列を一致させるにはLIKEを使います。次のワイルドカード文字がサポートされています。\n\n* `*`は0文字以上と一致します。\n* `?`は1文字と一致します。\n\n```\nFROM employees\n| WHERE first_name LIKE \"?b*\"\n| KEEP first_name, last_name\n```\n\n正規表現を使って文字列を一致させるには、RLIKEを使います。\n\n```\nFROM employees\n| WHERE first_name RLIKE \".leja.*\"\n| KEEP first_name, last_name\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.substring": "SUBSTRING", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.substring.markdown": "\n\n ### SUBSTRING\n 文字列のサブ文字列を、開始位置とオプションの長さで指定して返します。\n\n ```\n FROM employees\n | KEEP last_name\n | EVAL ln_sub = SUBSTRING(last_name, 1, 3)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.tan": "TAN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.tan.markdown": "\n\n ### TAN\n 角度の正接三角関数を返します。\n\n ```\n ROW a=1.8 \n | EVAL tan=TAN(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.tanh": "TANH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.tanh.markdown": "\n\n ### TANH\n 角度の正接双曲線関数を返します。\n\n ```\n ROW a=1.8 \n | EVAL tanh=TANH(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.tau": "TAU", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.tau.markdown": "\n\n ### TAU\n 円の円周と半径の比率を返します。\n\n ```\n ROW TAU()\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_base64": "TO_BASE64", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_base64.markdown": "\n\n ### TO_BASE64\n 文字列をbase64文字列にエンコードします。\n\n ```\n row a = \"elastic\" \n | eval e = to_base64(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_boolean": "TO_BOOLEAN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_boolean.markdown": "\n\n ### TO_BOOLEAN\n 入力値をブール値に変換します。\n 文字列値*true*は、大文字小文字を区別せずにブール値*true*に変換されます。\n 空文字列を含むそれ以外の値に対しては、この関数は*false*を返します。\n 数値*0*は*false*に変換され、それ以外は*true*に変換されます。\n\n ```\n ROW str = [\"true\", \"TRuE\", \"false\", \"\", \"yes\", \"1\"]\n | EVAL bool = TO_BOOLEAN(str)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_cartesianpoint": "TO_CARTESIANPOINT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_cartesianpoint.markdown": "\n\n ### TO_CARTESIANPOINT\n 入力値をcartesian_point値に変換します。\n 文字列は、WKT Point形式に従っている場合にのみ、正常に変換されます。\n\n ```\n ROW wkt = [\"POINT(4297.11 -1475.53)\", \"POINT(7580.93 2272.77)\"]\n | MV_EXPAND wkt\n | EVAL pt = TO_CARTESIANPOINT(wkt)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_cartesianshape": "TO_CARTESIANSHAPE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_cartesianshape.markdown": "\n\n ### TO_CARTESIANSHAPE\n 入力値をcartesian_shape値に変換します。\n 文字列は、WKT形式に従っている場合にのみ、正常に変換されます。\n\n ```\n ROW wkt = [\"POINT(4297.11 -1475.53)\", \"POLYGON ((3339584.72 1118889.97, 4452779.63 4865942.27, 2226389.81 4865942.27, 1113194.90 2273030.92, 3339584.72 1118889.97))\"]\n | MV_EXPAND wkt\n | EVAL geom = TO_CARTESIANSHAPE(wkt)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_datetime": "TO_DATETIME", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_datetime.markdown": "\n\n ### TO_DATETIME\n 入力値を日付値に変換します。\n 文字列は、yyyy-MM-dd'T'HH:mm:ss.SSS'Z'の書式に従っている場合のみ変換が成功します。\n 日付を他の形式に変換するには、DATE_PARSEを使用します。\n\n ```\n ROW string = [\"1953-09-02T00:00:00.000Z\", \"1964-06-02T00:00:00.000Z\", \"1964-06-02 00:00:00\"]\n | EVAL datetime = TO_DATETIME(string)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_degrees": "TO_DEGREES", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_degrees.markdown": "\n\n ### TO_DEGREES\n ラジアンの数値を度数に変換します。\n\n ```\n ROW rad = [1.57, 3.14, 4.71]\n | EVAL deg = TO_DEGREES(rad)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_double": "TO_DOUBLE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_double.markdown": "\n\n ### TO_DOUBLE\n 入力値をdouble値に変換します。入力パラメーターが日付型の場合、その値はUnixのエポックからのミリ秒として解釈され、doubleに変換されます。\n \n ブール値の*true*はdouble値の*1.0*に、*false*は*0.0*に変換されます。\n\n ```\n ROW str1 = \"5.20128E11\", str2 = \"foo\"\n | EVAL dbl = TO_DOUBLE(\"520128000000\"), dbl1 = TO_DOUBLE(str1), dbl2 = TO_DOUBLE(str2)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_geopoint": "TO_GEOPOINT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_geopoint.markdown": "\n\n ### TO_GEOPOINT\n 入力値をgeo_point値に変換します。\n 文字列は、WKT Point形式に従っている場合にのみ、正常に変換されます。\n\n ```\n ROW wkt = \"POINT(42.97109630194 14.7552534413725)\"\n | EVAL pt = TO_GEOPOINT(wkt)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_geoshape": "TO_GEOSHAPE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_geoshape.markdown": "\n\n ### TO_GEOSHAPE\n 入力値をgeo_shape値に変換します。\n 文字列は、WKT形式に従っている場合にのみ、正常に変換されます。\n\n ```\n ROW wkt = \"POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))\"\n | EVAL geom = TO_GEOSHAPE(wkt)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_integer": "TO_INTEGER", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_integer.markdown": "\n\n ### TO_INTEGER\n 入力値を整数値に変換します。\n 入力パラメーターが日付型の場合、その値はUnixのエポックからのミリ秒として解釈され、整数に変換されます。\n \n ブール値*true*は整数*1*に、*false*は*0*に変換されます。\n\n ```\n ROW long = [5013792, 2147483647, 501379200000]\n | EVAL int = TO_INTEGER(long)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_ip": "TO_IP", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_ip.markdown": "\n\n ### TO_IP\n 入力文字列をIP値に変換します。\n\n ```\n ROW str1 = \"1.1.1.1\", str2 = \"foo\"\n | EVAL ip1 = TO_IP(str1), ip2 = TO_IP(str2)\n | WHERE CIDR_MATCH(ip1, \"1.0.0.0/8\")\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_long": "TO_LONG", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_long.markdown": "\n\n ### TO_LONG\n 入力値をlong値に変換します。入力パラメーターが日付型の場合、\n その値はUnixのエポックからのミリ秒として解釈され、longに変換されます。\n ブール値の*true*は*long*値の*1*に、*false*は*0*に変換されます。\n\n ```\n ROW str1 = \"2147483648\", str2 = \"2147483648.2\", str3 = \"foo\"\n | EVAL long1 = TO_LONG(str1), long2 = TO_LONG(str2), long3 = TO_LONG(str3)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_lower": "TO_LOWER", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_lower.markdown": "\n\n ### TO_LOWER\n 小文字に変換された入力文字列を表す新しい文字列を返します。\n\n ```\n ROW message = \"Some Text\"\n | EVAL message_lower = TO_LOWER(message)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_radians": "TO_RADIANS", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_radians.markdown": "\n\n ### TO_RADIANS\n 度数をラジアンに変換します。\n\n ```\n ROW deg = [90.0, 180.0, 270.0]\n | EVAL rad = TO_RADIANS(deg)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_string": "TO_STRING", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_string.markdown": "\n\n ### TO_STRING\n 入力値を文字列に変換します。\n\n ```\n ROW a=10\n | EVAL j = TO_STRING(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_unsigned_long": "TO_UNSIGNED_LONG", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_unsigned_long.markdown": "\n\n ### TO_UNSIGNED_LONG\n 入力値を符号なしlong値に変換します。入力パラメーターが日付型の場合、\n その値はUnixのエポックからのミリ秒として解釈され、符号なしlong値に変換されます。\n ブール値の*true*は符号なし*long*値の*1*に、*false*は*0*に変換されます。\n\n ```\n ROW str1 = \"2147483648\", str2 = \"2147483648.2\", str3 = \"foo\"\n | EVAL long1 = TO_UNSIGNED_LONG(str1), long2 = TO_ULONG(str2), long3 = TO_UL(str3)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_upper": "TO_UPPER", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_upper.markdown": "\n\n ### TO_UPPER\n 大文字に変換された入力文字列を表す新しい文字列を返します。\n\n ```\n ROW message = \"Some Text\"\n | EVAL message_upper = TO_UPPER(message)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_version": "TO_VERSION", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_version.markdown": "\n\n ### TO_VERSION\n 入力文字列をバージョン値に変換します。\n\n ```\n ROW v = TO_VERSION(\"1.2.3\")\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.trim": "TRIM", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.trim.markdown": "\n\n ### TRIM\n 文字列から先頭と末尾の空白を削除します。\n\n ```\n ROW message = \" some text \", color = \" red \"\n | EVAL message = TRIM(message)\n | EVAL color = TRIM(color)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.where": "WHERE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.where.markdown": "### WHERE\nWHEREを使用すると、入力テーブルから、指定した条件がtrueと評価されるすべての行を含むテーブルを作成します。\n \n```\nFROM employees\n| KEEP first_name, last_name, still_hired\n| WHERE still_hired == true\n```\n\n#### 演算子\n\nサポートされている演算子の概要については、**演算子**を参照してください。\n\n#### 関数\nWHEREは値を計算するためのさまざまな関数をサポートしています。**関数**をクリックすると詳細が表示されます。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationLabel": "ドキュメント", + "languageDocumentationPopover.documentationESQL.abs": "ABS", + "languageDocumentationPopover.documentationESQL.abs.markdown": "\n\n ### ABS\n 絶対値を返します。\n\n ```\n ROW number = -1.0 \n | EVAL abs_number = ABS(number)\n ```\n ", + "languageDocumentationPopover.documentationESQL.acos": "ACOS", + "languageDocumentationPopover.documentationESQL.acos.markdown": "\n\n ### ACOS\n nのアークコサインをラジアンで表記された角度として返します。\n\n ```\n ROW a=.9\n | EVAL acos=ACOS(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.asin": "ASIN", + "languageDocumentationPopover.documentationESQL.asin.markdown": "\n\n ### ASIN\n 入力\n 数値式のアークサインをラジアンで表記された角度として返します。\n\n ```\n ROW a=.9\n | EVAL asin=ASIN(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.atan": "ATAN", + "languageDocumentationPopover.documentationESQL.atan.markdown": "\n\n ### ATAN\n 入力\n 数値式のアークタンジェントをラジアンで表記された角度として返します。\n\n ```\n ROW a=12.9\n | EVAL atan=ATAN(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.atan2": "ATAN2", + "languageDocumentationPopover.documentationESQL.atan2.markdown": "\n\n ### ATAN2\n 直交平面上の原点から点(x , y)に向かう光線と正のx軸のなす角(ラジアン表記)。\n \n\n ```\n ROW y=12.9, x=.6\n | EVAL atan2=ATAN2(y, x)\n ```\n ", + "languageDocumentationPopover.documentationESQL.autoBucketFunction": "BUCKET", + "languageDocumentationPopover.documentationESQL.autoBucketFunction.markdown": "### バケット\n日時または数値入力から、値(バケット)のグループを作成します。バケットのサイズは直接指定するか、推奨される数と値の範囲に基づいて選択できます。\n\nBUCKETは次の2つのモードで動作します。\n\n1.バケットのサイズがバケット数の提案(4つのパラメーター)と範囲に基づいて計算される。\n2.バケットサイズが直接指定される(2つのパラメーター)。\n\n目標バケット数、開始日、終了日を使用すると、目標バケット数以下のバケットを生成するために適切なバケットサイズがBUCKETによって選択されます。\n\nたとえば、1年に最大20バケットをリクエストすると、データが1か月間隔で整理されます。\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hire_date = MV_SORT(VALUES(hire_date)) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT hire_date\n```\n\n**注**:ここでは、正確な目標バケット数を指定するのではなく、目標バケット数を_上限_として範囲を指定します。\n\nBUCKETを集約と組み合わせ、ヒストグラムを作成できます。\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_month = COUNT(*) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT month\n```\n\n**注**:BUCKETは、どのドキュメントにも一致しないバケットを作成しません。そのため、前の例では1985-03-01やその他の日付が抜けています。\n\nその他のバケットを要求すると、範囲が小さくなることがあります。たとえば、1年に最大100バケットをリクエストすると、1週間単位のバケットになります。\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_week = COUNT(*) BY week = BUCKET(hire_date, 100, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT week\n```\n\n**注**:AUTO_BUCKETは行をフィルタリングしません。指定された範囲のみを使用して、適切なバケットサイズを選択します。範囲外の値の行に対しては、範囲外のバケツに対応するバケット値を返します。行をフィルタリングするには、BUCKETとWHEREを組み合わせます。\n\n事前に任意のバケットサイズがわかっている場合は、2番目の引数として指定し、範囲を除外します。\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_week = COUNT(*) BY week = BUCKET(hire_date, 1 week)\n| SORT week\n```\n\n**注**:バケットサイズを2番目のパラメーターとして指定するときには、時間の期間または日付の期間を選択する必要があります。\n\nBUCKETは数値フィールドでも動作します。たとえば、給与ヒストグラムを作成します。\n\n```\nFROM employees\n| STATS COUNT(*) by bs = BUCKET(salary, 20, 25324, 74999)\n| SORT bs\n```\n\n日付範囲で意図的フィルタリングする前の例とは異なり、数値フィールドでフィルタリングすることはほとんどありません。最小値と最大値を別々に見つける必要があります。ES|QLにはそれを自動的に実行するための簡単な方法がありません。\n\n任意のバケットサイズが事前にわかっている場合は、範囲を省略できます。2番目の引数として指定します。\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS c = COUNT(1) BY b = BUCKET(salary, 5000.)\n| SORT b\n```\n\n**注**:バケットサイズを2番目のパラメーターとして指定するときには、**浮動小数点数型**でなければなりません。\n\n次の例は、過去24時間の1時間単位のバケットを作成し、1時間当たりのイベント数を計算します。\n\n```\nFROM sample_data\n| WHERE @timestamp >= NOW() - 1 day and @timestamp < NOW()\n| STATS COUNT(*) BY bucket = BUCKET(@timestamp, 25, NOW() - 1 day, NOW())\n```\n\n次の例は、1985年の1か月単位のバケットを作成し、採用月別に平均給与を計算します。\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS AVG(salary) BY bucket = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT bucket\n```\n\n集約部で関数が**グループ部で定義されたエイリアスによって参照されている**場合、またはまったく同じ式で呼び出されている場合、BUCKETは、 STATS …​ BY …コマンドの集約部とグループ部の両方で使用できます。\n\n例:\n\n```\nFROM employees\n| STATS s1 = b1 + 1, s2 = BUCKET(salary / 1000 + 999, 50.) + 2 BY b1 = BUCKET(salary / 100 + 99, 50.), b2 = BUCKET(salary / 1000 + 999, 50.)\n| SORT b1, b2\n| KEEP s1, b1, s2, b2\n```\n ", + "languageDocumentationPopover.documentationESQL.binaryOperators": "バイナリ演算子", + "languageDocumentationPopover.documentationESQL.binaryOperators.markdown": "### バイナリ演算子\n次のバイナリ比較演算子がサポートされています。\n\n* 等号:`==`\n* 不等号:`!=`\n* より小さい:`<`\n* 以下:`<=`\n* より大きい:`>`\n* 以上:`>=`\n* 加算:`+`\n* 減算:`-`\n* 乗算:`*`\n* 除算:`/`\n* 係数:`%`\n ", + "languageDocumentationPopover.documentationESQL.booleanOperators": "ブール演算子", + "languageDocumentationPopover.documentationESQL.booleanOperators.markdown": "### ブール演算子\n次のブール演算子がサポートされています。\n\n* `AND`\n* `OR`\n* `NOT`\n ", + "languageDocumentationPopover.documentationESQL.bucket": "BUCKET", + "languageDocumentationPopover.documentationESQL.bucket.markdown": "\n\n ### BUCKET\n 日時または数値入力から、値(バケット)のグループを作成します。\n バケットのサイズは直接指定するか、推奨される数と値の範囲に基づいて選択できます。\n\n ```\n FROM employees\n | WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n | STATS hire_date = MV_SORT(VALUES(hire_date)) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n | SORT hire_date\n ```\n ", + "languageDocumentationPopover.documentationESQL.case": "CASE", + "languageDocumentationPopover.documentationESQL.case.markdown": "\n\n ### CASE\n 条件と値のペアを指定できます。この関数は、trueと評価される\n 最初の条件に属する値を返します。\n\n 引数の数が奇数の場合、最後の引数は条件に一致しない場合に返されるデフォルト値になります。\n 引数の数が偶数で、\n 条件が一致しない場合、この関数はnullを返します。\n\n ```\n FROM employees\n | EVAL type = CASE(\n languages <= 1, \"monolingual\",\n languages <= 2, \"bilingual\",\n \"polyglot\")\n | KEEP emp_no, languages, type\n ```\n ", + "languageDocumentationPopover.documentationESQL.castOperator": "Cast (::)", + "languageDocumentationPopover.documentationESQL.castOperator.markdown": "### CAST (`::`)\n::演算子はO_型変換関数に代わる便利な構文です。\n\n例:\n```\nROW ver = CONCAT((\"0\"::INT + 1)::STRING, \".2.3\")::VERSION\n```\n ", + "languageDocumentationPopover.documentationESQL.cbrt": "CBRT", + "languageDocumentationPopover.documentationESQL.cbrt.markdown": "\n\n ### CBRT\n 数値の立方根を返します。入力は任意の数値で、戻り値は常にdoubleです。\n 無限大の立方根はnullです。\n\n ```\n ROW d = 1000.0\n | EVAL c = cbrt(d)\n ```\n ", + "languageDocumentationPopover.documentationESQL.ceil": "CEIL", + "languageDocumentationPopover.documentationESQL.ceil.markdown": "\n\n ### CEIL\n 最も近い整数に数値を切り上げます。\n\n ```\n ROW a=1.8\n | EVAL a=CEIL(a)\n ```\n 注:これはlong(符号なしを含む)とintegerのnoopです。doubleの場合、JavaのMath.ceilと同様に、整数に最も近いdoubleの値を選びます。\n ", + "languageDocumentationPopover.documentationESQL.cidr_match": "CIDR_MATCH", + "languageDocumentationPopover.documentationESQL.cidr_match.markdown": "\n\n ### CIDR_MATCH\n 指定されたIPが指定されたCIDRブロックのいずれかに含まれていればtrueを返します。\n\n ```\n FROM hosts \n | WHERE CIDR_MATCH(ip1, \"127.0.0.2/32\", \"127.0.0.3/32\") \n | KEEP card, host, ip0, ip1\n ```\n ", + "languageDocumentationPopover.documentationESQL.coalesce": "COALESCE", + "languageDocumentationPopover.documentationESQL.coalesce.markdown": "\n\n ### COALESCE\n nullでない最初の引数を返します。すべての引数がnullの場合はnullを返します。\n\n ```\n ROW a=null, b=\"b\"\n | EVAL COALESCE(a, b)\n ```\n ", + "languageDocumentationPopover.documentationESQL.concat": "CONCAT", + "languageDocumentationPopover.documentationESQL.concat.markdown": "\n\n ### CONCAT\n 2つ以上の文字列を連結します。\n\n ```\n FROM employees\n | KEEP first_name, last_name\n | EVAL fullname = CONCAT(first_name, \" \", last_name)\n ```\n ", + "languageDocumentationPopover.documentationESQL.cos": "COS", + "languageDocumentationPopover.documentationESQL.cos.markdown": "\n\n ### COS\n 角度の余弦を返します。\n\n ```\n ROW a=1.8 \n | EVAL cos=COS(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.cosh": "COSH", + "languageDocumentationPopover.documentationESQL.cosh.markdown": "\n\n ### COSH\n 角度の双曲余弦を返します。\n\n ```\n ROW a=1.8 \n | EVAL cosh=COSH(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.date_diff": "DATE_DIFF", + "languageDocumentationPopover.documentationESQL.date_diff.markdown": "\n\n ### DATE_DIFF\n startTimestampをendTimestampから減算し、unitの乗数の差を返します。\n startTimestampがendTimestampより後の場合は、負の値が返されます。\n\n ```\n ROW date1 = TO_DATETIME(\"2023-12-02T11:00:00.000Z\"), date2 = TO_DATETIME(\"2023-12-02T11:00:00.001Z\")\n | EVAL dd_ms = DATE_DIFF(\"microseconds\", date1, date2)\n ```\n ", + "languageDocumentationPopover.documentationESQL.date_extract": "DATE_EXTRACT", + "languageDocumentationPopover.documentationESQL.date_extract.markdown": "\n\n ### DATE_EXTRACT\n 年、月、日、時間など、日付の一部を抽出します。\n\n ```\n ROW date = DATE_PARSE(\"yyyy-MM-dd\", \"2022-05-06\")\n | EVAL year = DATE_EXTRACT(\"year\", date)\n ```\n ", + "languageDocumentationPopover.documentationESQL.date_format": "DATE_FORMAT", + "languageDocumentationPopover.documentationESQL.date_format.markdown": "\n\n ### DATE_FORMAT\n 指定した書式の日付の文字列表現を返します。\n\n ```\n FROM employees\n | KEEP first_name, last_name, hire_date\n | EVAL hired = DATE_FORMAT(\"YYYY-MM-dd\", hire_date)\n ```\n ", + "languageDocumentationPopover.documentationESQL.date_parse": "DATE_PARSE", + "languageDocumentationPopover.documentationESQL.date_parse.markdown": "\n\n ### DATE_PARSE\n 最初の引数で指定した形式を使用して、2番目の引数を解析することで、日付を返します。\n\n ```\n ROW date_string = \"2022-05-06\"\n | EVAL date = DATE_PARSE(\"yyyy-MM-dd\", date_string)\n ```\n ", + "languageDocumentationPopover.documentationESQL.date_trunc": "DATE_TRUNC", + "languageDocumentationPopover.documentationESQL.date_trunc.markdown": "\n\n ### DATE_TRUNC\n 最も近い区間まで日付を切り捨てます。\n\n ```\n FROM employees\n | KEEP first_name, last_name, hire_date\n | EVAL year_hired = DATE_TRUNC(1 year, hire_date)\n ```\n ", + "languageDocumentationPopover.documentationESQL.dissect": "DISSECT", + "languageDocumentationPopover.documentationESQL.dissect.markdown": "### DISSECT\nDISSECTは文字列から構造化データを取り出すことができます。DISSECTは文字列を区切り文字ベースのパターンと照合し、指定されたキーを列として抽出します。\n\ndissectパターンの構文については、[dissectプロセッサードキュメント](https://www.elastic.co/guide/en/elasticsearch/reference/current/dissect-processor.html)を参照してください。\n\n```\nROW a = \"1953-01-23T12:15:00Z - some text - 127.0.0.1\"\n| DISSECT a \"%'{Y}-%{M}-%{D}T%{h}:%{m}:%{s}Z - %{msg} - %{ip}'\"\n``` ", + "languageDocumentationPopover.documentationESQL.drop": "DROP", + "languageDocumentationPopover.documentationESQL.drop.markdown": "### DROP\nテーブルから列を削除するには、DROPを使用します。\n \n```\nFROM employees\n| DROP height\n```\n\n各列を名前で指定するのではなく、ワイルドカードを使って、パターンと一致する名前の列をすべて削除することができます。\n\n```\nFROM employees\n| DROP height*\n```\n ", + "languageDocumentationPopover.documentationESQL.e": "E", + "languageDocumentationPopover.documentationESQL.e.markdown": "\n\n ### E\n オイラー数を返します。\n\n ```\n ROW E()\n ```\n ", + "languageDocumentationPopover.documentationESQL.ends_with": "ENDS_WITH", + "languageDocumentationPopover.documentationESQL.ends_with.markdown": "\n\n ### ENDS_WITH\n キーワード文字列が他の文字列で終わるかどうかを示すブール値を返します。\n\n ```\n FROM employees\n | KEEP last_name\n | EVAL ln_E = ENDS_WITH(last_name, \"d\")\n ```\n ", + "languageDocumentationPopover.documentationESQL.enrich": "ENRICH", + "languageDocumentationPopover.documentationESQL.enrich.markdown": "### ENRICH\nENRICH`を使用すると、既存のインデックスのデータを受信レコードに追加することができます。[インジェストエンリッチ](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html)と似ていますが、クエリー時に動作します。\n\n```\nROW language_code = \"1\"\n| ENRICH languages_policy\n```\n\nENRICHでは、[エンリッチポリシー](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html#enrich-policy)を実行する必要があります。エンリッチポリシーは、一致フィールド (キーフィールド) とエンリッチフィールドのセットを定義します。\n\nENRICHは、一致フィールド値に基づいて、[エンリッチインデックス](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html#enrich-index)のレコードを検索します。入力データセットの一致するキーは、ON を使用して定義できます。指定しない場合は、エンリッチポリシーで定義された一致フィールドと同じ名前のフィールドで一致が実行されます。\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a\n```\n\nWITH , ...構文を使用して、結果に追加される属性(ポリシーでエンリッチフィールドとして定義された属性の間)を指定できます。\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a WITH language_name\n```\n\n属性の名前は、WITH new_name=を使用して変更できます。\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a WITH name = language_name\n```\n\nデフォルトでは、(WITHが定義されていない場合)、ENRICHはエンリッチポリシーで定義されたすべてのエンリッチフィールドを結果に追加します。\n\n名前の競合が発生した場合、新しく作成されたフィールドが既存のフィールドを上書きします。\n ", + "languageDocumentationPopover.documentationESQL.eval": "EVAL", + "languageDocumentationPopover.documentationESQL.eval.markdown": "### EVAL\nEVALでは、新しい列を追加できます。\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| EVAL height_feet = height * 3.281, height_cm = height * 100\n```\n\n指定した列がすでに存在する場合、既存の列は削除され、新しい列がテーブルに追加されます。\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| EVAL height = height * 3.281\n```\n\n#### 関数\nEVALは値を計算するためのさまざまな関数をサポートしています。関数をクリックすると詳細が表示されます。\n ", + "languageDocumentationPopover.documentationESQL.floor": "FLOOR", + "languageDocumentationPopover.documentationESQL.floor.markdown": "\n\n ### FLOOR\n 最も近い整数に数値を切り捨てます。\n\n ```\n ROW a=1.8\n | EVAL a=FLOOR(a)\n ```\n 注:これはlong(符号なしを含む)とintegerのnoopです。\n doubleの場合、Math.floorと同様に、整数に最も近いdoubleの値を選びます。\n \n ", + "languageDocumentationPopover.documentationESQL.from": "FROM", + "languageDocumentationPopover.documentationESQL.from_base64": "FROM_BASE64", + "languageDocumentationPopover.documentationESQL.from_base64.markdown": "\n\n ### FROM_BASE64\n base64文字列をデコードします。\n\n ```\n row a = \"ZWxhc3RpYw==\" \n | eval d = from_base64(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.from.markdown": "### FROM\nソースコマンドFROMは、データストリーム、インデックス、またはエイリアスから、最大10,000ドキュメントを含むテーブルを返します。結果のテーブルの各行はドキュメントを表します。各列はフィールドに対応し、そのフィールドの名前でアクセスできます。\n\n```\nFROM employees\n```\n\n[日付演算](https://www.elastic.co/guide/en/elasticsearch/reference/current/api-conventions.html#api-date-math-index-names) を使用して、インデックス、エイリアス、データストリームを参照できます。これは時系列データの場合に便利です。\n\nカンマ区切りのリストまたはワイルドカードを使用して、複数のデータストリーム、インデックス、またはエイリアスをクエリーします。\n\n```\nFROM employees-00001,employees-*\n```\n\n#### メタデータ\n\nES|QLは以下のメタデータフィールドにアクセスできます。\n\n* `_index`:ドキュメントが属するインデックス。このフィールドはkeyword型です。\n* `_id`:ソースドキュメントのID。このフィールドはkeyword型です。### `_version`:ソースドキュメントのバージョン。フィールドの型はlongです。\n\nメタデータフィールドを有効にするには、METADATAディレクティブを使います。\n\n```\nFROM index [METADATA _index, _id]\n```\n\nメタデータフィールドは、データのソースがインデックスである場合にのみ使用できます。その結果、FROMはMETADATAディレクティブをサポートする唯一のソースコマンドです。\n\nこのフィールドが有効になると、他のインデックスフィールドと同様に、後続の処理コマンドで利用できるようになります。\n\n```\nFROM ul_logs, apps [METADATA _index, _version]\n| WHERE id IN (13, 14) AND _version == 1\n| EVAL key = CONCAT(_index, \"_\", TO_STR(id))\n| SORT id, _index\n| KEEP id, _index, _version, key\n```\n\nまた、インデックス・フィールドと同様に、一度集約が実行されると、グループ化フィールドとして使用されないかぎり、メタデータフィールドは後続のコマンドからはアクセスできなくなります。\n\n```\nFROM employees [METADATA _index, _id]\n| STATS max = MAX(emp_no) BY _index\n```\n ", + "languageDocumentationPopover.documentationESQL.greatest": "GREATEST", + "languageDocumentationPopover.documentationESQL.greatest.markdown": "\n\n ### GREATEST\n 多数の列から最大値を返します。これはMV_MAX\n と似ていますが、一度に複数の列に対して実行します。\n\n ```\n ROW a = 10, b = 20\n | EVAL g = GREATEST(a, b)\n ```\n 注:keywordまたはtextフィールドに対して実行すると、アルファベット順の最後の文字列を返します。boolean列に対して実行すると、値がtrueの場合にtrueを返します。\n ", + "languageDocumentationPopover.documentationESQL.grok": "GROK", + "languageDocumentationPopover.documentationESQL.grok.markdown": "### GROK\nGROKを使うと、文字列から構造化データを抽出できます。GROKは正規表現に基づいて文字列をパターンと一致させ、指定されたパターンを列として抽出します。\n\ngrokパターンの構文については、 [grokプロセッサードキュメント](https://www.elastic.co/guide/en/elasticsearch/reference/current/grok-processor.html)を参照してください。\n\n```\nROW a = \"12 15.5 15.6 true\"\n| GROK a \"%'{NUMBER:b:int}' %'{NUMBER:c:float}' %'{NUMBER:d:double}' %'{WORD:e:boolean}'\"\n```\n ", + "languageDocumentationPopover.documentationESQL.inOperator": "IN", + "languageDocumentationPopover.documentationESQL.inOperator.markdown": "### IN\nIN演算子は、フィールドや式がリテラル、フィールド、式のリストの要素と等しいかどうかをテストすることができます。\n\n```\nROW a = 1, b = 4, c = 3\n| WHERE c-a IN (3, b / 2, a)\n```\n ", + "languageDocumentationPopover.documentationESQL.ip_prefix": "IP_PREFIX", + "languageDocumentationPopover.documentationESQL.ip_prefix.markdown": "\n\n ### IP_PREFIX\n IPを特定のプレフィックス長に切り詰めます。\n\n ```\n row ip4 = to_ip(\"1.2.3.4\"), ip6 = to_ip(\"fe80::cae2:65ff:fece:feb9\")\n | eval ip4_prefix = ip_prefix(ip4, 24, 0), ip6_prefix = ip_prefix(ip6, 0, 112);\n ```\n ", + "languageDocumentationPopover.documentationESQL.keep": "KEEP", + "languageDocumentationPopover.documentationESQL.keep.markdown": "### KEEP\nKEEPコマンドは、返される列と、列が返される順序を指定することができます。\n\n返される列を制限するには、カンマで区切りの列名リストを使用します。列は指定された順序で返されます。\n \n```\nFROM employees\n| KEEP first_name, last_name, height\n```\n\n各列を名前で指定するのではなく、ワイルドカードを使って、パターンと一致する名前の列をすべて返すことができます。\n\n```\nFROM employees\n| KEEP h*\n```\n\nアスタリスクワイルドカード(*)は単独で、他の引数と一致しないすべての列に変換されます。このクエリーは、最初にhで始まる名前の列をすべて返し、その後にその他の列をすべて返します。\n\n```\nFROM employees\n| KEEP h*, *\n```\n ", + "languageDocumentationPopover.documentationESQL.least": "LEAST", + "languageDocumentationPopover.documentationESQL.least.markdown": "\n\n ### LEAST\n 多数の列から最小値を返します。これはMV_MINと似ていますが、一度に複数の列に対して実行します。\n\n ```\n ROW a = 10, b = 20\n | EVAL l = LEAST(a, b)\n ```\n ", + "languageDocumentationPopover.documentationESQL.left": "LEFT", + "languageDocumentationPopover.documentationESQL.left.markdown": "\n\n ### LEFT\n stringから左から順にlength文字を抜き出したサブ文字列を返します。\n\n ```\n FROM employees\n | KEEP last_name\n | EVAL left = LEFT(last_name, 3)\n | SORT last_name ASC\n | LIMIT 5\n ```\n ", + "languageDocumentationPopover.documentationESQL.length": "LENGTH", + "languageDocumentationPopover.documentationESQL.length.markdown": "\n\n ### LENGTH\n 文字列の文字数を返します。\n\n ```\n FROM employees\n | KEEP first_name, last_name\n | EVAL fn_length = LENGTH(first_name)\n ```\n ", + "languageDocumentationPopover.documentationESQL.limit": "LIMIT", + "languageDocumentationPopover.documentationESQL.limit.markdown": "### LIMIT\nLIMIT`処理コマンドは行数を制限することができます。\n \n```\nFROM employees\n| LIMIT 5\n```\n ", + "languageDocumentationPopover.documentationESQL.locate": "LOCATE", + "languageDocumentationPopover.documentationESQL.locate.markdown": "\n\n ### LOCATE\n 別の文字列内のキーワードサブ文字列の位置を示す整数を返します。\n\n ```\n row a = \"hello\"\n | eval a_ll = locate(a, \"ll\")\n ```\n ", + "languageDocumentationPopover.documentationESQL.log": "LOG", + "languageDocumentationPopover.documentationESQL.log.markdown": "\n\n ### LOG\n 基数に対する値の対数を返します。入力は任意の数値で、戻り値は常にdoubleです。\n\n ゼロの対数、負数、1の基数はnullと警告を返します。\n\n ```\n ROW base = 2.0, value = 8.0\n | EVAL s = LOG(base, value)\n ```\n ", + "languageDocumentationPopover.documentationESQL.log10": "LOG10", + "languageDocumentationPopover.documentationESQL.log10.markdown": "\n\n ### LOG10\n 基数10に対する値の対数を返します。入力は任意の数値で、戻り値は常にdoubleです。\n\n 0の対数および負数はnullと警告を返します。\n\n ```\n ROW d = 1000.0 \n | EVAL s = LOG10(d)\n ```\n ", + "languageDocumentationPopover.documentationESQL.ltrim": "LTRIM", + "languageDocumentationPopover.documentationESQL.ltrim.markdown": "\n\n ### LTRIM\n 文字列から先頭の空白を取り除きます。\n\n ```\n ROW message = \" some text \", color = \" red \"\n | EVAL message = LTRIM(message)\n | EVAL color = LTRIM(color)\n | EVAL message = CONCAT(\"'\", message, \"'\")\n | EVAL color = CONCAT(\"'\", color, \"'\")\n ```\n ", + "languageDocumentationPopover.documentationESQL.markdown": "## ES|QL\n\nES|QL(Elasticsearch クエリー言語)クエリーは、パイプ文字の|で区切られた一連のコマンドで構成されます。各クエリーは**ソースコマンド**で始まり、通常はElasticsearchのデータを使ってテーブルを生成します。\n\nソースコマンドには、1つ以上の**処理コマンド**を続けることができます。処理コマンドは、行や列を追加、削除、変更することで、前のコマンドの出力テーブルを変更することができます。\n\n```\nsource-command\n| processing-command1\n| processing-command2\n```\n\nクエリーの結果は、最終的な処理コマンドによって生成されるテーブルです。 \n ", + "languageDocumentationPopover.documentationESQL.mv_append": "MV_APPEND", + "languageDocumentationPopover.documentationESQL.mv_append.markdown": "\n\n ### MV_APPEND\n 2つの複数値フィールドの値を連結します\n\n ", + "languageDocumentationPopover.documentationESQL.mv_avg": "MV_AVG", + "languageDocumentationPopover.documentationESQL.mv_avg.markdown": "\n\n ### MV_AVG\n 複数値フィールドを、すべての値の平均を含む単一値フィールドに変換します。\n\n ```\n ROW a=[3, 5, 1, 6]\n | EVAL avg_a = MV_AVG(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_concat": "MV_CONCAT", + "languageDocumentationPopover.documentationESQL.mv_concat.markdown": "\n\n ### MV_CONCAT\n 複数値文字列式を、区切り文字で区切られたすべての値を連結した単一値列に変換します。\n\n ```\n ROW a=[\"foo\", \"zoo\", \"bar\"]\n | EVAL j = MV_CONCAT(a, \", \")\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_count": "MV_COUNT", + "languageDocumentationPopover.documentationESQL.mv_count.markdown": "\n\n ### MV_COUNT\n 複数値式を、値の数をカウントする単一値列に変換します。\n\n ```\n ROW a=[\"foo\", \"zoo\", \"bar\"]\n | EVAL count_a = MV_COUNT(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_dedupe": "MV_DEDUPE", + "languageDocumentationPopover.documentationESQL.mv_dedupe.markdown": "\n\n ### MV_DEDUPE\n 複数値フィールドから重複する値を削除します。\n\n ```\n ROW a=[\"foo\", \"foo\", \"bar\", \"foo\"]\n | EVAL dedupe_a = MV_DEDUPE(a)\n ```\n 注:MV_DEDUPEは列の値をソートすることがありますが、常にソートするわけではありません。\n ", + "languageDocumentationPopover.documentationESQL.mv_first": "MV_FIRST", + "languageDocumentationPopover.documentationESQL.mv_first.markdown": "\n\n ### MV_FIRST\n \n 複数値式を、最初の値を含む単一値列に変換します。これは、SPLITなどの既知の順序で複数値列を発行する関数から読み取るときに役立ちます。\n \n\n 複数値フィールドが基本ストレージから読み取られる順序は保証されません。\n \n 通常は昇順ですが、必ずしもそうであるわけではありません。最小値が必要な場合は、MV_FIRSTの代わりに、MV_MINを使用します。\n MV_MINは、ソートされた値向けに最適化されているため、\n MV_FIRSTにパフォーマンスの利点はありません。\n\n ```\n ROW a=\"foo;bar;baz\"\n | EVAL first_a = MV_FIRST(SPLIT(a, \";\"))\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_last": "MV_LAST", + "languageDocumentationPopover.documentationESQL.mv_last.markdown": "\n\n ### MV_LAST\n \n 複数値式を、最後の値を含む単一値列に変換します。これは、SPLITなどの既知の順序で複数値列を発行する関数から読み取るときに役立ちます。\n \n\n 複数値フィールドが基本ストレージから読み取られる順序は保証されません。\n \n 通常は昇順ですが、必ずしもそうであるわけではありません。最大値が必要な場合は、MV_LASTの代わりに、MV_MAXを使用します。\n MV_MAXは、ソートされた値向けに最適化されているため、\n MV_LASTにパフォーマンスの利点はありません。\n\n ```\n ROW a=\"foo;bar;baz\"\n | EVAL last_a = MV_LAST(SPLIT(a, \";\"))\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_max": "MV_MAX", + "languageDocumentationPopover.documentationESQL.mv_max.markdown": "\n\n ### MV_MAX\n 複数値フィールドを、最大値を含む単一値フィールドに変換します。\n\n ```\n ROW a=[3, 5, 1]\n | EVAL max_a = MV_MAX(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_median": "MV_MEDIAN", + "languageDocumentationPopover.documentationESQL.mv_median.markdown": "\n\n ### MV_MEDIAN\n 複数値フィールドを、中央値を含む単一値フィールドに変換します。\n\n ```\n ROW a=[3, 5, 1]\n | EVAL median_a = MV_MEDIAN(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_min": "MV_MIN", + "languageDocumentationPopover.documentationESQL.mv_min.markdown": "\n\n ### MV_MIN\n 複数値フィールドを、最小値を含む単一値フィールドに変換します。\n\n ```\n ROW a=[2, 1]\n | EVAL min_a = MV_MIN(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_slice": "MV_SLICE", + "languageDocumentationPopover.documentationESQL.mv_slice.markdown": "\n\n ### MV_SLICE\n 開始インデックス値と終了インデックス値を使用して、複数値フィールドのサブセットを返します。\n\n ```\n row a = [1, 2, 2, 3]\n | eval a1 = mv_slice(a, 1), a2 = mv_slice(a, 2, 3)\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_sort": "MV_SORT", + "languageDocumentationPopover.documentationESQL.mv_sort.markdown": "\n\n ### MV_SORT\n 辞書の順序で複数値フィールドを並べ替えます。\n\n ```\n ROW a = [4, 2, -3, 2]\n | EVAL sa = mv_sort(a), sd = mv_sort(a, \"DESC\")\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_sum": "MV_SUM", + "languageDocumentationPopover.documentationESQL.mv_sum.markdown": "\n\n ### MV_SUM\n 複数値フィールドを、すべての値の合計を含む単一値フィールドに変換します。\n\n ```\n ROW a=[3, 5, 6]\n | EVAL sum_a = MV_SUM(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_zip": "MV_ZIP", + "languageDocumentationPopover.documentationESQL.mv_zip.markdown": "\n\n ### MV_ZIP\n 値を結合する区切り文字を使用して、2つの複数値フィールドの値を結合します。\n\n ```\n ROW a = [\"x\", \"y\", \"z\"], b = [\"1\", \"2\"]\n | EVAL c = mv_zip(a, b, \"-\")\n | KEEP a, b, c\n ```\n ", + "languageDocumentationPopover.documentationESQL.mvExpand": "MV_EXPAND", + "languageDocumentationPopover.documentationESQL.mvExpand.markdown": "### MV_EXPAND\nMV_EXPAND処理コマンドは、複数値フィールドを値ごとに1行に展開し、他のフィールドを複製します。 \n```\nROW a=[1,2,3], b=\"b\", j=[\"a\",\"b\"]\n| MV_EXPAND a\n```\n ", + "languageDocumentationPopover.documentationESQL.now": "NOW", + "languageDocumentationPopover.documentationESQL.now.markdown": "\n\n ### NOW\n 現在の日付と時刻を返します。\n\n ```\n ROW current_date = NOW()\n ```\n ", + "languageDocumentationPopover.documentationESQL.pi": "PI", + "languageDocumentationPopover.documentationESQL.pi.markdown": "\n\n ### PI\n 円の円周と直径の比率であるPiを返します。\n\n ```\n ROW PI()\n ```\n ", + "languageDocumentationPopover.documentationESQL.pow": "POW", + "languageDocumentationPopover.documentationESQL.pow.markdown": "\n\n ### POW\n exponentのべき乗にしたbaseの値を返します。\n\n ```\n ROW base = 2.0, exponent = 2\n | EVAL result = POW(base, exponent)\n ```\n 注:ここでは、倍精度浮動小数点数の結果でもオーバーフローする可能性があります。その場合は、NULLが返されます。\n ", + "languageDocumentationPopover.documentationESQL.predicates": "NULL値", + "languageDocumentationPopover.documentationESQL.predicates.markdown": "### NULL値\nNULLの比較には、IS NULLとIS NOT NULL述語を使います。\n\n```\nFROM employees\n| WHERE birth_date IS NULL\n| KEEP first_name, last_name\n| SORT first_name\n| LIMIT 3\n```\n\n```\nFROM employees\n| WHERE is_rehired IS NOT NULL\n| STATS count(emp_no)\n```\n ", + "languageDocumentationPopover.documentationESQL.rename": "RENAME", + "languageDocumentationPopover.documentationESQL.rename.markdown": "### RENAME\nRENAMEを使用して、次の構文で列の名前を変更します。\n\n```\nRENAME AS \n```\n\n例:\n\n```\nFROM employees\n| KEEP first_name, last_name, still_hired\n| RENAME still_hired AS employed\n```\n\n新しい名前の列がすでに存在する場合、その列は新しい列に置き換えられます。\n\n複数の列の名前を1つのRENAMEコマンドで変更することができます。\n\n```\nFROM employees\n| KEEP first_name, last_name\n| RENAME first_name AS fn, last_name AS ln\n```\n ", + "languageDocumentationPopover.documentationESQL.repeat": "REPEAT", + "languageDocumentationPopover.documentationESQL.repeat.markdown": "\n\n ### REPEAT\n 指定したnumberの回数、文字列stringとそれ自身を連結して構成された文字列を返します。\n\n ```\n ROW a = \"Hello!\"\n | EVAL triple_a = REPEAT(a, 3);\n ```\n ", + "languageDocumentationPopover.documentationESQL.replace": "REPLACE", + "languageDocumentationPopover.documentationESQL.replace.markdown": "\n\n ### REPLACE\n \n この関数は、正規表現regexと置換文字列newStrの任意の一致を文字列strに代入します。\n\n ```\n ROW str = \"Hello World\"\n | EVAL str = REPLACE(str, \"World\", \"Universe\")\n | KEEP str\n ```\n ", + "languageDocumentationPopover.documentationESQL.right": "RIGHT", + "languageDocumentationPopover.documentationESQL.right.markdown": "\n\n ### RIGHT\n strのうち右から数えてlength文字までのサブ文字列を返します。\n\n ```\n FROM employees\n | KEEP last_name\n | EVAL right = RIGHT(last_name, 3)\n | SORT last_name ASC\n | LIMIT 5\n ```\n ", + "languageDocumentationPopover.documentationESQL.round": "ROUND", + "languageDocumentationPopover.documentationESQL.round.markdown": "\n\n ### ROUND\n 数値を指定した小数点以下の桁数に丸めます。\n デフォルトは0で、最も近い整数を返します。\n 精度が負の場合、小数点以下の桁数に丸めます。\n \n\n ```\n FROM employees\n | KEEP first_name, last_name, height\n | EVAL height_ft = ROUND(height * 3.281, 1)\n ```\n ", + "languageDocumentationPopover.documentationESQL.row": "ROW", + "languageDocumentationPopover.documentationESQL.row.markdown": "### ROW\nROWソースコマンドは、指定した値の列を1つ以上含む行を作成します。これはテストの場合に便利です。\n \n```\nROW a = 1, b = \"two\", c = null\n```\n\n複数の値を含む列を作成するには角括弧を使用します。\n\n```\nROW a = [2, 1]\n```\n\nROWは関数の使用をサポートしています。\n\n```\nROW a = ROUND(1.23, 0)\n```\n ", + "languageDocumentationPopover.documentationESQL.rtrim": "RTRIM", + "languageDocumentationPopover.documentationESQL.rtrim.markdown": "\n\n ### RTRIM\n 文字列から末尾の空白を取り除きます。\n\n ```\n ROW message = \" some text \", color = \" red \"\n | EVAL message = RTRIM(message)\n | EVAL color = RTRIM(color)\n | EVAL message = CONCAT(\"'\", message, \"'\")\n | EVAL color = CONCAT(\"'\", color, \"'\")\n ```\n ", + "languageDocumentationPopover.documentationESQL.show": "SHOW", + "languageDocumentationPopover.documentationESQL.show.markdown": "### SHOW\nSHOW ソースコマンドはデプロイとその能力に関する情報を返します。\n\n* デプロイのバージョン、ビルド日、ハッシュを返すには、SHOW INFOを使用します。\n* SHOW FUNCTIONSを使用すると、サポートされているすべての関数のリストと各関数の概要を返します。\n ", + "languageDocumentationPopover.documentationESQL.signum": "SIGNUM", + "languageDocumentationPopover.documentationESQL.signum.markdown": "\n\n ### SIGNUM\n 任意の数値の符号を返します。\n 負の数値の場合は-1を返します。0の場合は0を返します。正の数値の場合は1を返します。\n\n ```\n ROW d = 100.0\n | EVAL s = SIGNUM(d)\n ```\n ", + "languageDocumentationPopover.documentationESQL.sin": "SIN", + "languageDocumentationPopover.documentationESQL.sin.markdown": "\n\n ### SIN\n 角度の正弦三角関数を返します。\n\n ```\n ROW a=1.8 \n | EVAL sin=SIN(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.sinh": "SINH", + "languageDocumentationPopover.documentationESQL.sinh.markdown": "\n\n ### SINH\n 角度の双曲線正弦を返します。\n\n ```\n ROW a=1.8 \n | EVAL sinh=SINH(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.sort": "SORT", + "languageDocumentationPopover.documentationESQL.sort.markdown": "### SORT\nSORTコマンドを使用すると、1つ以上のフィールドで行をソートすることができます。\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height\n```\n\nデフォルトのソート順は昇順です。ASCまたはDESCを使って明示的なソート順を設定します。\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height DESC\n```\n\n2つの行のソートキーが同じ場合、元の順序が保持されます。タイブレーカーとなるソート式を追加で指定できます。\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height DESC, first_name ASC\n```\n\n#### null値\nデフォルトでは、null値は他のどの値よりも大きい値として扱われます。昇順のソートではnull値は最後にソートされ、降順のソートではnull値は最初にソートされます。NULLS FIRSTまたはNULLS LASTを指定することで変更できます。\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| SORT first_name ASC NULLS FIRST\n```\n ", + "languageDocumentationPopover.documentationESQL.split": "SPLIT", + "languageDocumentationPopover.documentationESQL.split.markdown": "\n\n ### SPLIT\n 単一の値の文字列を複数の文字列に分割します。\n\n ```\n ROW words=\"foo;bar;baz;qux;quux;corge\"\n | EVAL word = SPLIT(words, \";\")\n ```\n ", + "languageDocumentationPopover.documentationESQL.sqrt": "SQRT", + "languageDocumentationPopover.documentationESQL.sqrt.markdown": "\n\n ### SQRT\n 数値の平方根を返します。入力は任意の数値で、戻り値は常にdoubleです。\n 負数と無限大の平方根はnullです。\n\n ```\n ROW d = 100.0\n | EVAL s = SQRT(d)\n ```\n ", + "languageDocumentationPopover.documentationESQL.st_contains": "ST_CONTAINS", + "languageDocumentationPopover.documentationESQL.st_contains.markdown": "\n\n ### ST_CONTAINS\n 最初のジオメトリに2番目のジオメトリが含まれるかどうかを返します。\n これはST_WITHIN関数の逆関数です。\n\n ```\n FROM airport_city_boundaries\n | WHERE ST_CONTAINS(city_boundary, TO_GEOSHAPE(\"POLYGON((109.35 18.3, 109.45 18.3, 109.45 18.4, 109.35 18.4, 109.35 18.3))\"))\n | KEEP abbrev, airport, region, city, city_location\n ```\n ", + "languageDocumentationPopover.documentationESQL.st_disjoint": "ST_DISJOINT", + "languageDocumentationPopover.documentationESQL.st_disjoint.markdown": "\n\n ### ST_DISJOINT\n 2つのジオメトリまたはジオメトリ列が結合解除されているかどうかを返します。\n これはST_INTERSECTS関数の逆関数です。\n 数学的には次のようになります。ST_Disjoint(A, B) ⇔ A ⋂ B = ∅\n\n ```\n FROM airport_city_boundaries\n | WHERE ST_DISJOINT(city_boundary, TO_GEOSHAPE(\"POLYGON((-10 -60, 120 -60, 120 60, -10 60, -10 -60))\"))\n | KEEP abbrev, airport, region, city, city_location\n ```\n ", + "languageDocumentationPopover.documentationESQL.st_distance": "ST_DISTANCE", + "languageDocumentationPopover.documentationESQL.st_distance.markdown": "\n\n ### ST_DISTANCE\n 2点間の距離を計算します。\n デカルト幾何学の場合、これは元の座標と同じ単位でのピタゴラス距離です。\n 地理的幾何学では、これはメートル単位での円に沿った円周距離です。\n\n ```\n FROM airports\n | WHERE abbrev == \"CPH\"\n | EVAL distance = ST_DISTANCE(location, city_location)\n | KEEP abbrev, name, location, city_location, distance\n ```\n ", + "languageDocumentationPopover.documentationESQL.st_intersects": "ST_INTERSECTS", + "languageDocumentationPopover.documentationESQL.st_intersects.markdown": "\n\n ### ST_INTERSECTS\n 2つのジオメトリが交差している場合はTrueを返します。\n 内部点を含め、共通の点がある場合は交差しています\n (線に沿った点または多角形内の点)。\n これはST_DISJOINT関数の逆関数です。\n 数学的には次のようになります。ST_Intersects(A, B) ⇔ A ⋂ B ≠ ∅\n\n ```\n FROM airports\n | WHERE ST_INTERSECTS(location, TO_GEOSHAPE(\"POLYGON((42 14, 43 14, 43 15, 42 15, 42 14))\"))\n ```\n ", + "languageDocumentationPopover.documentationESQL.st_within": "ST_WITHIN", + "languageDocumentationPopover.documentationESQL.st_within.markdown": "\n\n ### ST_WITHIN\n 最初のジオメトリが2番目のジオメトリ内にあるかどうかを返します。\n これはST_CONTAINS関数の逆関数です。\n\n ```\n FROM airport_city_boundaries\n | WHERE ST_WITHIN(city_boundary, TO_GEOSHAPE(\"POLYGON((109.1 18.15, 109.6 18.15, 109.6 18.65, 109.1 18.65, 109.1 18.15))\"))\n | KEEP abbrev, airport, region, city, city_location\n ```\n ", + "languageDocumentationPopover.documentationESQL.st_x": "ST_X", + "languageDocumentationPopover.documentationESQL.st_x.markdown": "\n\n ### ST_X\n 指定された点からx座標を抽出します。\n この点がgeo_pointタイプの場合は、longitude値を抽出するのと同じ結果になります。\n\n ```\n ROW point = TO_GEOPOINT(\"POINT(42.97109629958868 14.7552534006536)\")\n | EVAL x = ST_X(point), y = ST_Y(point)\n ```\n ", + "languageDocumentationPopover.documentationESQL.st_y": "ST_Y", + "languageDocumentationPopover.documentationESQL.st_y.markdown": "\n\n ### ST_Y\n 指定された点からy座標を抽出します。\n この点がgeo_pointタイプの場合は、latitude値を抽出するのと同じ結果になります。\n\n ```\n ROW point = TO_GEOPOINT(\"POINT(42.97109629958868 14.7552534006536)\")\n | EVAL x = ST_X(point), y = ST_Y(point)\n ```\n ", + "languageDocumentationPopover.documentationESQL.starts_with": "STARTS_WITH", + "languageDocumentationPopover.documentationESQL.starts_with.markdown": "\n\n ### STARTS_WITH\n キーワード文字列が他の文字列で始まるかどうかを示すブール値を返します。\n\n ```\n FROM employees\n | KEEP last_name\n | EVAL ln_S = STARTS_WITH(last_name, \"B\")\n ```\n ", + "languageDocumentationPopover.documentationESQL.statsby": "STATS ...BY", + "languageDocumentationPopover.documentationESQL.statsby.markdown": "### STATS ...BY\nSTATS ...BYを使用すると、共通の値に従って行をグループ化し、グループ化された行に対する1つ以上の集約値を計算します。\n\n**例**:\n\n```\nFROM employees\n| STATS count = COUNT(emp_no) BY languages\n| SORT languages\n```\n\nBYが省略された場合、出力テーブルには、データセット全体に適用された集約が正確に1行だけ含まれます。\n\n```\nFROM employees\n| STATS avg_lang = AVG(languages)\n```\n\n複数の値を計算することができます。\n\n```\nFROM employees\n| STATS avg_lang = AVG(languages), max_lang = MAX(languages)\n```\n\n複数の値でグループ化することも可能です(longおよびkeywordファミリーフィールドでのみサポート)。\n\n```\nFROM employees\n| EVAL hired = DATE_FORMAT(hire_date, \"YYYY\")\n| STATS avg_salary = AVG(salary) BY hired, languages.long\n| EVAL avg_salary = ROUND(avg_salary)\n| SORT hired, languages.long\n```\n\nSTATS ...BYで使用できる関数の一覧については、**集計関数**を参照してください。\n\n集計関数とグループ式の両方で他の関数を使用できます。これは、複数値列でSTATS...BYを使用するときに有用です。たとえば、平均給与変動を計算するには、まず、MV_AVGを使用して従業員ごとに複数の値の平均を求め、その結果にAVG関数を適用します。\n\n```\nFROM employees\n| STATS avg_salary_change = AVG(MV_AVG(salary_change))\n```\n\n式によるグループ化の例は、姓の最初の文字で従業員をグループ化することです。\n\n```\nFROM employees\n| STATS my_count = COUNT() BY LEFT(last_name, 1)\n| SORT `LEFT(last_name, 1)`\n```\n\n出力列名の指定は任意です。指定しない場合は、新しい列名が式と等しくなります。次のクエリーは列\"AVG(salary)\"を返します。\n\n```\nFROM employees\n| STATS AVG(salary)\n```\n\nこの名前には特殊文字が含まれているため、後続のコマンドで使用するときには、バッククオート(`)で囲む必要があります。\n\n```\nFROM employees\n| STATS AVG(salary)\n| EVAL avg_salary_rounded = ROUND(`AVG(salary)`)\n```\n\n**注**:グループなしのSTATSは、グループを追加するよりも大幅に高速です。\n\n**注**:単一式でのグループは、現在、複数式でのグループよりも大幅に最適化されています。\n ", + "languageDocumentationPopover.documentationESQL.stringOperators": "LIKEおよびRLIKE", + "languageDocumentationPopover.documentationESQL.stringOperators.markdown": "### LIKEおよびRLIKE\nワイルドカードや正規表現を使った文字列比較にはLIKEまたはRLIKEを使います。\n\nワイルドカードを使って文字列を一致させるにはLIKEを使います。次のワイルドカード文字がサポートされています。\n\n* `*`は0文字以上と一致します。\n* `?`は1文字と一致します。\n\n```\nFROM employees\n| WHERE first_name LIKE \"?b*\"\n| KEEP first_name, last_name\n```\n\n正規表現を使って文字列を一致させるには、RLIKEを使います。\n\n```\nFROM employees\n| WHERE first_name RLIKE \".leja.*\"\n| KEEP first_name, last_name\n```\n ", + "languageDocumentationPopover.documentationESQL.substring": "SUBSTRING", + "languageDocumentationPopover.documentationESQL.substring.markdown": "\n\n ### SUBSTRING\n 文字列のサブ文字列を、開始位置とオプションの長さで指定して返します。\n\n ```\n FROM employees\n | KEEP last_name\n | EVAL ln_sub = SUBSTRING(last_name, 1, 3)\n ```\n ", + "languageDocumentationPopover.documentationESQL.tan": "TAN", + "languageDocumentationPopover.documentationESQL.tan.markdown": "\n\n ### TAN\n 角度の正接三角関数を返します。\n\n ```\n ROW a=1.8 \n | EVAL tan=TAN(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.tanh": "TANH", + "languageDocumentationPopover.documentationESQL.tanh.markdown": "\n\n ### TANH\n 角度の正接双曲線関数を返します。\n\n ```\n ROW a=1.8 \n | EVAL tanh=TANH(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.tau": "TAU", + "languageDocumentationPopover.documentationESQL.tau.markdown": "\n\n ### TAU\n 円の円周と半径の比率を返します。\n\n ```\n ROW TAU()\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_base64": "TO_BASE64", + "languageDocumentationPopover.documentationESQL.to_base64.markdown": "\n\n ### TO_BASE64\n 文字列をbase64文字列にエンコードします。\n\n ```\n row a = \"elastic\" \n | eval e = to_base64(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_boolean": "TO_BOOLEAN", + "languageDocumentationPopover.documentationESQL.to_boolean.markdown": "\n\n ### TO_BOOLEAN\n 入力値をブール値に変換します。\n 文字列値*true*は、大文字小文字を区別せずにブール値*true*に変換されます。\n 空文字列を含むそれ以外の値に対しては、この関数は*false*を返します。\n 数値*0*は*false*に変換され、それ以外は*true*に変換されます。\n\n ```\n ROW str = [\"true\", \"TRuE\", \"false\", \"\", \"yes\", \"1\"]\n | EVAL bool = TO_BOOLEAN(str)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_cartesianpoint": "TO_CARTESIANPOINT", + "languageDocumentationPopover.documentationESQL.to_cartesianpoint.markdown": "\n\n ### TO_CARTESIANPOINT\n 入力値をcartesian_point値に変換します。\n 文字列は、WKT Point形式に従っている場合にのみ、正常に変換されます。\n\n ```\n ROW wkt = [\"POINT(4297.11 -1475.53)\", \"POINT(7580.93 2272.77)\"]\n | MV_EXPAND wkt\n | EVAL pt = TO_CARTESIANPOINT(wkt)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_cartesianshape": "TO_CARTESIANSHAPE", + "languageDocumentationPopover.documentationESQL.to_cartesianshape.markdown": "\n\n ### TO_CARTESIANSHAPE\n 入力値をcartesian_shape値に変換します。\n 文字列は、WKT形式に従っている場合にのみ、正常に変換されます。\n\n ```\n ROW wkt = [\"POINT(4297.11 -1475.53)\", \"POLYGON ((3339584.72 1118889.97, 4452779.63 4865942.27, 2226389.81 4865942.27, 1113194.90 2273030.92, 3339584.72 1118889.97))\"]\n | MV_EXPAND wkt\n | EVAL geom = TO_CARTESIANSHAPE(wkt)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_datetime": "TO_DATETIME", + "languageDocumentationPopover.documentationESQL.to_datetime.markdown": "\n\n ### TO_DATETIME\n 入力値を日付値に変換します。\n 文字列は、yyyy-MM-dd'T'HH:mm:ss.SSS'Z'の書式に従っている場合のみ変換が成功します。\n 日付を他の形式に変換するには、DATE_PARSEを使用します。\n\n ```\n ROW string = [\"1953-09-02T00:00:00.000Z\", \"1964-06-02T00:00:00.000Z\", \"1964-06-02 00:00:00\"]\n | EVAL datetime = TO_DATETIME(string)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_degrees": "TO_DEGREES", + "languageDocumentationPopover.documentationESQL.to_degrees.markdown": "\n\n ### TO_DEGREES\n ラジアンの数値を度数に変換します。\n\n ```\n ROW rad = [1.57, 3.14, 4.71]\n | EVAL deg = TO_DEGREES(rad)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_double": "TO_DOUBLE", + "languageDocumentationPopover.documentationESQL.to_double.markdown": "\n\n ### TO_DOUBLE\n 入力値をdouble値に変換します。入力パラメーターが日付型の場合、その値はUnixのエポックからのミリ秒として解釈され、doubleに変換されます。\n \n ブール値の*true*はdouble値の*1.0*に、*false*は*0.0*に変換されます。\n\n ```\n ROW str1 = \"5.20128E11\", str2 = \"foo\"\n | EVAL dbl = TO_DOUBLE(\"520128000000\"), dbl1 = TO_DOUBLE(str1), dbl2 = TO_DOUBLE(str2)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_geopoint": "TO_GEOPOINT", + "languageDocumentationPopover.documentationESQL.to_geopoint.markdown": "\n\n ### TO_GEOPOINT\n 入力値をgeo_point値に変換します。\n 文字列は、WKT Point形式に従っている場合にのみ、正常に変換されます。\n\n ```\n ROW wkt = \"POINT(42.97109630194 14.7552534413725)\"\n | EVAL pt = TO_GEOPOINT(wkt)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_geoshape": "TO_GEOSHAPE", + "languageDocumentationPopover.documentationESQL.to_geoshape.markdown": "\n\n ### TO_GEOSHAPE\n 入力値をgeo_shape値に変換します。\n 文字列は、WKT形式に従っている場合にのみ、正常に変換されます。\n\n ```\n ROW wkt = \"POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))\"\n | EVAL geom = TO_GEOSHAPE(wkt)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_integer": "TO_INTEGER", + "languageDocumentationPopover.documentationESQL.to_integer.markdown": "\n\n ### TO_INTEGER\n 入力値を整数値に変換します。\n 入力パラメーターが日付型の場合、その値はUnixのエポックからのミリ秒として解釈され、整数に変換されます。\n \n ブール値*true*は整数*1*に、*false*は*0*に変換されます。\n\n ```\n ROW long = [5013792, 2147483647, 501379200000]\n | EVAL int = TO_INTEGER(long)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_ip": "TO_IP", + "languageDocumentationPopover.documentationESQL.to_ip.markdown": "\n\n ### TO_IP\n 入力文字列をIP値に変換します。\n\n ```\n ROW str1 = \"1.1.1.1\", str2 = \"foo\"\n | EVAL ip1 = TO_IP(str1), ip2 = TO_IP(str2)\n | WHERE CIDR_MATCH(ip1, \"1.0.0.0/8\")\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_long": "TO_LONG", + "languageDocumentationPopover.documentationESQL.to_long.markdown": "\n\n ### TO_LONG\n 入力値をlong値に変換します。入力パラメーターが日付型の場合、\n その値はUnixのエポックからのミリ秒として解釈され、longに変換されます。\n ブール値の*true*は*long*値の*1*に、*false*は*0*に変換されます。\n\n ```\n ROW str1 = \"2147483648\", str2 = \"2147483648.2\", str3 = \"foo\"\n | EVAL long1 = TO_LONG(str1), long2 = TO_LONG(str2), long3 = TO_LONG(str3)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_lower": "TO_LOWER", + "languageDocumentationPopover.documentationESQL.to_lower.markdown": "\n\n ### TO_LOWER\n 小文字に変換された入力文字列を表す新しい文字列を返します。\n\n ```\n ROW message = \"Some Text\"\n | EVAL message_lower = TO_LOWER(message)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_radians": "TO_RADIANS", + "languageDocumentationPopover.documentationESQL.to_radians.markdown": "\n\n ### TO_RADIANS\n 度数をラジアンに変換します。\n\n ```\n ROW deg = [90.0, 180.0, 270.0]\n | EVAL rad = TO_RADIANS(deg)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_string": "TO_STRING", + "languageDocumentationPopover.documentationESQL.to_string.markdown": "\n\n ### TO_STRING\n 入力値を文字列に変換します。\n\n ```\n ROW a=10\n | EVAL j = TO_STRING(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_unsigned_long": "TO_UNSIGNED_LONG", + "languageDocumentationPopover.documentationESQL.to_unsigned_long.markdown": "\n\n ### TO_UNSIGNED_LONG\n 入力値を符号なしlong値に変換します。入力パラメーターが日付型の場合、\n その値はUnixのエポックからのミリ秒として解釈され、符号なしlong値に変換されます。\n ブール値の*true*は符号なし*long*値の*1*に、*false*は*0*に変換されます。\n\n ```\n ROW str1 = \"2147483648\", str2 = \"2147483648.2\", str3 = \"foo\"\n | EVAL long1 = TO_UNSIGNED_LONG(str1), long2 = TO_ULONG(str2), long3 = TO_UL(str3)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_upper": "TO_UPPER", + "languageDocumentationPopover.documentationESQL.to_upper.markdown": "\n\n ### TO_UPPER\n 大文字に変換された入力文字列を表す新しい文字列を返します。\n\n ```\n ROW message = \"Some Text\"\n | EVAL message_upper = TO_UPPER(message)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_version": "TO_VERSION", + "languageDocumentationPopover.documentationESQL.to_version.markdown": "\n\n ### TO_VERSION\n 入力文字列をバージョン値に変換します。\n\n ```\n ROW v = TO_VERSION(\"1.2.3\")\n ```\n ", + "languageDocumentationPopover.documentationESQL.trim": "TRIM", + "languageDocumentationPopover.documentationESQL.trim.markdown": "\n\n ### TRIM\n 文字列から先頭と末尾の空白を削除します。\n\n ```\n ROW message = \" some text \", color = \" red \"\n | EVAL message = TRIM(message)\n | EVAL color = TRIM(color)\n ```\n ", + "languageDocumentationPopover.documentationESQL.where": "WHERE", + "languageDocumentationPopover.documentationESQL.where.markdown": "### WHERE\nWHEREを使用すると、入力テーブルから、指定した条件がtrueと評価されるすべての行を含むテーブルを作成します。\n \n```\nFROM employees\n| KEEP first_name, last_name, still_hired\n| WHERE still_hired == true\n```\n\n#### 演算子\n\nサポートされている演算子の概要については、**演算子**を参照してください。\n\n#### 関数\nWHEREは値を計算するためのさまざまな関数をサポートしています。**関数**をクリックすると詳細が表示されます。\n ", "textBasedEditor.query.textBasedLanguagesEditor.EnableWordWrapLabel": "パイプの改行を追加", "textBasedEditor.query.textBasedLanguagesEditor.errorCount": "{count} {count, plural, other {# 件のエラー}}", "textBasedEditor.query.textBasedLanguagesEditor.errorsTitle": "エラー", - "textBasedEditor.query.textBasedLanguagesEditor.esql": "ES|QL", "textBasedEditor.query.textBasedLanguagesEditor.expandLabel": "拡張", "textBasedEditor.query.textBasedLanguagesEditor.feedback": "フィードバック", - "textBasedEditor.query.textBasedLanguagesEditor.functions": "関数", - "textBasedEditor.query.textBasedLanguagesEditor.functionsDocumentationESQLDescription": "関数はROW、EVAL、WHEREでサポートされています。", - "textBasedEditor.query.textBasedLanguagesEditor.groupingFunctions": "グループ関数", - "textBasedEditor.query.textBasedLanguagesEditor.groupingFunctionsDocumentationESQLDescription": "これらのグループ関数はSTATS...BYで使用できます。", + "languageDocumentationPopover.documentationESQL.functions": "関数", + "languageDocumentationPopover.documentationESQL.functionsDocumentationESQLDescription": "関数はROW、EVAL、WHEREでサポートされています。", + "languageDocumentationPopover.documentationESQL.groupingFunctions": "グループ関数", + "languageDocumentationPopover.documentationESQL.groupingFunctionsDocumentationESQLDescription": "これらのグループ関数はSTATS...BYで使用できます。", "textBasedEditor.query.textBasedLanguagesEditor.hideQueriesLabel": "最近のクエリーを非表示", "textBasedEditor.query.textBasedLanguagesEditor.lineCount": "{count} {count, plural, other {行}}", "textBasedEditor.query.textBasedLanguagesEditor.lineNumber": "行{lineNumber}", - "textBasedEditor.query.textBasedLanguagesEditor.operators": "演算子", - "textBasedEditor.query.textBasedLanguagesEditor.operatorsDocumentationESQLDescription": "ES|QLは以下の演算子をサポートしています。", - "textBasedEditor.query.textBasedLanguagesEditor.processingCommands": "処理コマンド", - "textBasedEditor.query.textBasedLanguagesEditor.processingCommandsDescription": "処理コマンドは、行や列を追加、削除、変更することによって入力テーブルを変更します。ES|QLは以下の処理コマンドをサポートしています。", + "languageDocumentationPopover.documentationESQL.operators": "演算子", + "languageDocumentationPopover.documentationESQL.operatorsDocumentationESQLDescription": "ES|QLは以下の演算子をサポートしています。", + "languageDocumentationPopover.documentationESQL.processingCommands": "処理コマンド", + "languageDocumentationPopover.documentationESQL.processingCommandsDescription": "処理コマンドは、行や列を追加、削除、変更することによって入力テーブルを変更します。ES|QLは以下の処理コマンドをサポートしています。", "textBasedEditor.query.textBasedLanguagesEditor.querieshistory.error": "クエリ失敗", "textBasedEditor.query.textBasedLanguagesEditor.querieshistory.success": "クエリは正常に実行されました", "textBasedEditor.query.textBasedLanguagesEditor.querieshistoryCopy": "クエリをクリップボードにコピー", @@ -7334,7 +7332,7 @@ "textBasedEditor.query.textBasedLanguagesEditor.recentQueriesColumnLabel": "最近のクエリー", "textBasedEditor.query.textBasedLanguagesEditor.runQuery": "クエリーを実行", "textBasedEditor.query.textBasedLanguagesEditor.showQueriesLabel": "最近のクエリを表示", - "textBasedEditor.query.textBasedLanguagesEditor.sourceCommands": "ソースコマンド", + "languageDocumentationPopover.documentationESQL.sourceCommands": "ソースコマンド", "textBasedEditor.query.textBasedLanguagesEditor.submitFeedback": "フィードバックを送信", "textBasedEditor.query.textBasedLanguagesEditor.timeRanColumnLabel": "実行時間", "textBasedEditor.query.textBasedLanguagesEditor.timestampNotDetected": "@timestampが見つかりません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 16173ad84bf6a..5f697ce440ed8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7090,255 +7090,253 @@ "telemetry.usageCollectionConstant": "使用情况收集", "telemetry.usageDataTitle": "使用情况收集", "textBasedEditor.query.textBasedLanguagesEditor.aborted": "请求已中止", - "textBasedEditor.query.textBasedLanguagesEditor.aggregationFunctions": "聚合函数", - "textBasedEditor.query.textBasedLanguagesEditor.aggregationFunctionsDocumentationESQLDescription": "这些函数可以与 STATS...BY 搭配使用:", + "languageDocumentationPopover.documentationESQL.aggregationFunctions": "聚合函数", + "languageDocumentationPopover.documentationESQL.aggregationFunctionsDocumentationESQLDescription": "这些函数可以与 STATS...BY 搭配使用:", "textBasedEditor.query.textBasedLanguagesEditor.cancel": "取消", "textBasedEditor.query.textBasedLanguagesEditor.collapseLabel": "折叠", - "textBasedEditor.query.textBasedLanguagesEditor.commandsDescription": "源命令会生成一个表,其中通常包含来自 Elasticsearch 的数据。ES|QL 支持以下源命令。", + "languageDocumentationPopover.documentationESQL.commandsDescription": "源命令会生成一个表,其中通常包含来自 Elasticsearch 的数据。ES|QL 支持以下源命令。", "textBasedEditor.query.textBasedLanguagesEditor.disableWordWrapLabel": "移除管道符上的换行符", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.abs": "ABS", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.abs.markdown": "\n\n ### ABS\n 返回绝对值。\n\n ```\n ROW number = -1.0 \n | EVAL abs_number = ABS(number)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.acos": "ACOS", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.acos.markdown": "\n\n ### ACOS\n 返回 `n` 的反余弦作为角度,以弧度表示。\n\n ```\n ROW a=.9\n | EVAL acos=ACOS(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.asin": "ASIN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.asin.markdown": "\n\n ### ASIN\n 返回输入数字表达式的反正弦\n 作为角度,以弧度表示。\n\n ```\n ROW a=.9\n | EVAL asin=ASIN(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.atan": "ATAN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.atan.markdown": "\n\n ### ATAN\n 返回输入数字表达式的反正切\n 作为角度,以弧度表示。\n\n ```\n ROW a=12.9\n | EVAL atan=ATAN(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.atan2": "ATAN2", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.atan2.markdown": "\n\n ### ATAN2\n 笛卡儿平面中正 x 轴\n 与从原点到点 (x , y) 构成的射线之间的角度,以弧度表示。\n\n ```\n ROW y=12.9, x=.6\n | EVAL atan2=ATAN2(y, x)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.autoBucketFunction": "BUCKET", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.autoBucketFunction.markdown": "### BUCKET\n用日期时间或数字输入创建值(存储桶)的分组。存储桶的大小可以直接提供,或基于建议的计数和值范围进行选择。\n\n`BUCKET` 以两种模式运行:\n\n1.在此模式下基于存储桶计数建议(四个参数)和范围计算存储桶的大小。\n2.在此模式下直接提供存储桶大小(两个参数)。\n\n使用存储桶的目标数量、起始范围和结束范围,`BUCKET` 将选取适当的存储桶大小以生成目标数量或更小数量的存储桶。\n\n例如,一年请求多达 20 个存储桶会按每月时间间隔组织数据:\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hire_date = MV_SORT(VALUES(hire_date)) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT hire_date\n```\n\n**注意**:目标并不是提供存储桶的确切目标数量,而是选择一个范围,最多提供存储桶的目标数量。\n\n可以组合 `BUCKET` 与聚合以创建直方图:\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_month = COUNT(*) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT month\n```\n\n**注意**:`BUCKET` 不会创建未匹配任何文档的存储桶。因此,上一示例缺少 `1985-03-01` 和其他日期。\n\n如果需要更多存储桶,可能导致更小的范围。例如,如果一年内最多请求 100 个存储桶,会导致周期为周的存储桶:\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_week = COUNT(*) BY week = BUCKET(hire_date, 100, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT week\n```\n\n**注意**:`AUTO_BUCKET` 不筛选任何行。它只会使用提供的范围来选取适当的存储桶大小。对于值超出范围的行,它会返回与超出范围的存储桶对应的存储桶值。组合 `BUCKET` 与 `WHERE` 可筛选行。\n\n如果提前已知所需存储桶大小,则只需提供它作为第二个参数,而忽略范围:\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_week = COUNT(*) BY week = BUCKET(hire_date, 1 week)\n| SORT week\n```\n\n**注意**:提供存储桶大小作为第二个参数时,它必须为持续时间或日期期间。\n\n`BUCKET` 还可对数字字段执行操作。例如,要创建工资直方图:\n\n```\nFROM employees\n| STATS COUNT(*) by bs = BUCKET(salary, 20, 25324, 74999)\n| SORT bs\n```\n\n与前面的有意筛选日期范围示例不同,您极少想要筛选数值范围。您必须分别查找最小值和最大值。ES|QL 尚未提供简便方法来自动执行此操作。\n\n如果提前已知所需存储桶大小,则可以忽略该范围。只需提供它作为第二个参数即可:\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS c = COUNT(1) BY b = BUCKET(salary, 5000.)\n| SORT b\n```\n\n**注意**:提供存储桶大小作为第二个参数时,它必须为 **浮点类型**。\n\n这里提供了一个示例,用于为过去 24 小时创建小时存储桶,并计算每小时的事件数:\n\n```\nFROM sample_data\n| WHERE @timestamp >= NOW() - 1 day and @timestamp < NOW()\n| STATS COUNT(*) BY bucket = BUCKET(@timestamp, 25, NOW() - 1 day, NOW())\n```\n\n这里提供了一个示例,用于为 1985 年创建月度存储桶,并按聘用月份计算平均工资:\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS AVG(salary) BY bucket = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT bucket\n```\n\n`BUCKET` 可用在 `STATS …​ BY …`​ 命令的聚合和分组部分, 前提是在聚合部分中,该函数 **由在分组部分中定义的别名引用**,或使用完全相同的表达式调用。\n\n例如:\n\n```\nFROM employees\n| STATS s1 = b1 + 1, s2 = BUCKET(salary / 1000 + 999, 50.) + 2 BY b1 = BUCKET(salary / 100 + 99, 50.), b2 = BUCKET(salary / 1000 + 999, 50.)\n| SORT b1, b2\n| KEEP s1, b1, s2, b2\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.binaryOperators": "二进制运算符", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.binaryOperators.markdown": "### 二进制运算符\n支持这些二进制比较运算符:\n\n* 等于:`==`\n* 不等于:`!=`\n* 小于:`<`\n* 小于或等于:`<=`\n* 大于:`>`\n* 大于或等于:`>=`\n* 加:`+`\n* 减:`-`\n* 乘:`*`\n* 除:`/`\n* 取模:`%`\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.booleanOperators": "布尔运算符", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.booleanOperators.markdown": "### 布尔运算符\n支持以下布尔运算符:\n\n* `AND`\n* `OR`\n* `NOT`\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.bucket": "BUCKET", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.bucket.markdown": "\n\n ### BUCKET\n 用日期时间或数字输入创建值(存储桶)的分组。\n 存储桶的大小可以直接提供,或基于建议的计数和值范围进行选择。\n\n ```\n FROM employees\n | WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n | STATS hire_date = MV_SORT(VALUES(hire_date)) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n | SORT hire_date\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.case": "CASE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.case.markdown": "\n\n ### CASE\n 接受成对的条件和值。此函数返回属于第一个\n 评估为 `true` 的条件的值。\n\n 如果参数数量为奇数,则最后一个参数为\n 在无条件匹配时返回的默认值。如果参数数量为偶数,且\n 无任何条件匹配,则此函数返回 `null`。\n\n ```\n FROM employees\n | EVAL type = CASE(\n languages <= 1, \"monolingual\",\n languages <= 2, \"bilingual\",\n \"polyglot\")\n | KEEP emp_no, languages, type\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.castOperator": "Cast (::)", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.castOperator.markdown": "### CAST (`::`)\n`::` 运算符为 `TO_` 类型转换函数提供了实用的替代语法。\n\n例如:\n```\nROW ver = CONCAT((\"0\"::INT + 1)::STRING, \".2.3\")::VERSION\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cbrt": "CBRT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cbrt.markdown": "\n\n ### CBRT\n 返回数字的立方根。输入可以为任何数字值,返回值始终为双精度值。\n 无穷大的立方根为 null。\n\n ```\n ROW d = 1000.0\n | EVAL c = cbrt(d)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ceil": "CEIL", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ceil.markdown": "\n\n ### CEIL\n 将数字四舍五入为最近的整数。\n\n ```\n ROW a=1.8\n | EVAL a=CEIL(a)\n ```\n 注意:对于 `long`(包括无符号值)和 `integer`,这相当于“无操作”。对于 `double`,这会提取最接近整数的 `double` 值,类似于 Math.ceil。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cidr_match": "CIDR_MATCH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cidr_match.markdown": "\n\n ### CIDR_MATCH\n 如果提供的 IP 包含在所提供的其中一个 CIDR 块中,则返回 true。\n\n ```\n FROM hosts \n | WHERE CIDR_MATCH(ip1, \"127.0.0.2/32\", \"127.0.0.3/32\") \n | KEEP card, host, ip0, ip1\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.coalesce": "COALESCE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.coalesce.markdown": "\n\n ### COALESCE\n 返回它的第一个不为 null 的参数。如果所有参数均为 null,则返回 `null`。\n\n ```\n ROW a=null, b=\"b\"\n | EVAL COALESCE(a, b)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.concat": "CONCAT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.concat.markdown": "\n\n ### CONCAT\n 串联两个或多个字符串。\n\n ```\n FROM employees\n | KEEP first_name, last_name\n | EVAL fullname = CONCAT(first_name, \" \", last_name)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cos": "COS", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cos.markdown": "\n\n ### COS\n 返回角度的余弦。\n\n ```\n ROW a=1.8 \n | EVAL cos=COS(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cosh": "COSH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.cosh.markdown": "\n\n ### COSH\n 返回角度的双曲余弦。\n\n ```\n ROW a=1.8 \n | EVAL cosh=COSH(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_diff": "DATE_DIFF", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_diff.markdown": "\n\n ### DATE_DIFF\n 从 `endTimestamp` 中减去 `startTimestamp`,并以倍数 `unit` 返回差异。\n 如果 `startTimestamp` 晚于 `endTimestamp`,则返回负值。\n\n ```\n ROW date1 = TO_DATETIME(\"2023-12-02T11:00:00.000Z\"), date2 = TO_DATETIME(\"2023-12-02T11:00:00.001Z\")\n | EVAL dd_ms = DATE_DIFF(\"microseconds\", date1, date2)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_extract": "DATE_EXTRACT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_extract.markdown": "\n\n ### DATE_EXTRACT\n 提取日期的某些部分,如年、月、日、小时。\n\n ```\n ROW date = DATE_PARSE(\"yyyy-MM-dd\", \"2022-05-06\")\n | EVAL year = DATE_EXTRACT(\"year\", date)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_format": "DATE_FORMAT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_format.markdown": "\n\n ### DATE_FORMAT\n 以提供的格式返回日期的字符串表示形式。\n\n ```\n FROM employees\n | KEEP first_name, last_name, hire_date\n | EVAL hired = DATE_FORMAT(\"YYYY-MM-dd\", hire_date)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_parse": "DATE_PARSE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_parse.markdown": "\n\n ### DATE_PARSE\n 通过使用在第一个参数中指定的格式来解析第二个参数,从而返回日期。\n\n ```\n ROW date_string = \"2022-05-06\"\n | EVAL date = DATE_PARSE(\"yyyy-MM-dd\", date_string)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_trunc": "DATE_TRUNC", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.date_trunc.markdown": "\n\n ### DATE_TRUNC\n 将日期向下舍入到最近的时间间隔。\n\n ```\n FROM employees\n | KEEP first_name, last_name, hire_date\n | EVAL year_hired = DATE_TRUNC(1 year, hire_date)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.dissect": "DISSECT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.dissect.markdown": "### DISSECT\n使用 `DISSECT`,您可以从字符串中提取结构化数据。`DISSECT` 将根据基于分隔符的模式来匹配字符串,并提取指定键作为列。\n\n请参阅[分解处理器文档](https://www.elastic.co/guide/en/elasticsearch/reference/current/dissect-processor.html)了解分解模式的语法。\n\n```\nROW a = \"1953-01-23T12:15:00Z - some text - 127.0.0.1\"\n| DISSECT a \"%'{Y}-%{M}-%{D}T%{h}:%{m}:%{s}Z - %{msg} - %{ip}'\"\n``` ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.drop": "DROP", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.drop.markdown": "### DROP\n使用 `DROP` 可从表中移除列:\n \n```\nFROM employees\n| DROP height\n```\n\n您不必按名称指定每个列,而可以使用通配符丢弃名称匹配某种模式的所有列:\n\n```\nFROM employees\n| DROP height*\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.e": "E", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.e.markdown": "\n\n ### E\n 返回 Euler 函数的编号。\n\n ```\n ROW E()\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ends_with": "ENDS_WITH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ends_with.markdown": "\n\n ### ENDS_WITH\n 返回布尔值,指示关键字字符串是否以另一个字符串结尾。\n\n ```\n FROM employees\n | KEEP last_name\n | EVAL ln_E = ENDS_WITH(last_name, \"d\")\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.enrich": "ENRICH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.enrich.markdown": "### ENRICH\n您可以使用 `ENRICH` 将来自现有索引的数据添加到传入记录中。它类似于[采集扩充](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html),但作用于查询时间。\n\n```\nROW language_code = \"1\"\n| ENRICH languages_policy\n```\n\n执行 `ENRICH` 需要[扩充策略](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html#enrich-policy)。扩充策略定义一个匹配字段(键字段)和一组扩充字段。\n\n`ENRICH` 将根据匹配字段值在[扩充索引](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html#enrich-index)中查找记录。输入数据集中的匹配键可以使用 `ON ` 来定义;如果未指定,将对字段名称与在扩充策略中定义的匹配字段相同的字段执行匹配。\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a\n```\n\n您可以使用 `WITH , ...` 语法指定必须将哪些属性(在那些在策略中定义为扩充字段的字段之间)添加到结果中。\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a WITH language_name\n```\n\n还可以使用 `WITH new_name=` 重命名属性\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a WITH name = language_name\n```\n\n默认情况下(如果未定义任何 `WITH`),`ENRICH` 会将在扩充策略中定义的所有扩充字段添加到结果中。\n\n如果出现名称冲突,新创建的字段将覆盖现有字段。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.eval": "EVAL", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.eval.markdown": "### EVAL\n`EVAL` 允许您添加新列:\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| EVAL height_feet = height * 3.281, height_cm = height * 100\n```\n\n如果指定列已存在,将丢弃现有列,并将新列追加到表后面:\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| EVAL height = height * 3.281\n```\n\n#### 函数\n`EVAL` 支持各种用于计算值的函数。请参阅“函数”了解更多信息。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.floor": "FLOOR", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.floor.markdown": "\n\n ### FLOOR\n 将数字向下舍入到最近的整数。\n\n ```\n ROW a=1.8\n | EVAL a=FLOOR(a)\n ```\n 注意:对于 `long`(包括无符号值)和 `integer`,这相当于“无操作”。\n 对于 `double`,这会提取最接近整数的 `double` 值,\n 类似于 Math.floor。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.from": "FROM", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.from_base64": "FROM_BASE64", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.from_base64.markdown": "\n\n ### FROM_BASE64\n 解码 base64 字符串。\n\n ```\n row a = \"ZWxhc3RpYw==\" \n | eval d = from_base64(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.from.markdown": "### FROM\n`FROM` 源命令返回一个表,其中最多包含 10,000 个来自数据流、索引或别名的文档。生成的表中的每一行代表一个文档。每一列对应一个字段,并可以通过该字段的名称进行访问。\n\n```\nFROM employees\n```\n\n您可以使用[日期数学表达式](https://www.elastic.co/guide/en/elasticsearch/reference/current/api-conventions.html#api-date-math-index-names)来引用索引、别名和数据流。这可能对时间序列数据非常有用。\n\n使用逗号分隔列表或通配符可查询多个数据流、索引或别名:\n\n```\nFROM employees-00001,employees-*\n```\n\n#### 元数据\n\nES|QL 可访问以下元数据字段:\n\n* `_index`:文档所属的索引。字段类型为 `keyword`.\n* `_id`:源文档的 ID。字段类型为 `keyword`.\n* `_version`:源文档的版本。字段类型为 `long`。\n\n使用 `METADATA` 指令可启用元数据字段:\n\n```\nFROM index [METADATA _index, _id]\n```\n\n元数据字段仅在数据源为索引时可用。因此,`FROM` 是唯一支持 `METADATA` 指令的源命令。\n\n启用后,这些字段将可用于后续处理命令,就像其他索引字段一样:\n\n```\nFROM ul_logs, apps [METADATA _index, _version]\n| WHERE id IN (13, 14) AND _version == 1\n| EVAL key = CONCAT(_index, \"_\", TO_STR(id))\n| SORT id, _index\n| KEEP id, _index, _version, key\n```\n\n此外,与索引字段类似,一旦执行了聚合,后续命令将无法再访问元数据字段,除非它用作分组字段:\n\n```\nFROM employees [METADATA _index, _id]\n| STATS max = MAX(emp_no) BY _index\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.greatest": "GREATEST", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.greatest.markdown": "\n\n ### GREATEST\n 返回多个列中的最大值。除了可一次对多个列运行以外,\n 此函数与 `MV_MAX` 类似。\n\n ```\n ROW a = 10, b = 20\n | EVAL g = GREATEST(a, b)\n ```\n 注意:对 `keyword` 或 `text` 字段运行时,此函数将按字母顺序返回最后一个字符串。对 `boolean` 列运行时,如果任何值为 `true`,此函数将返回 `true`。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.grok": "GROK", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.grok.markdown": "### GROK\n使用 `GROK`,您可以从字符串中提取结构化数据。`GROK` 将基于正则表达式根据模式来匹配字符串,并提取指定模式作为列。\n\n请参阅 [grok 处理器文档](https://www.elastic.co/guide/en/elasticsearch/reference/current/grok-processor.html)了解 grok 模式的语法。\n\n```\nROW a = \"12 15.5 15.6 true\"\n| GROK a \"%'{NUMBER:b:int}' %'{NUMBER:c:float}' %'{NUMBER:d:double}' %'{WORD:e:boolean}'\"\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.inOperator": "IN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.inOperator.markdown": "### IN\n`IN` 运算符允许测试字段或表达式是否等于文本、字段或表达式列表中的元素:\n\n```\nROW a = 1, b = 4, c = 3\n| WHERE c-a IN (3, b / 2, a)\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ip_prefix": "IP_PREFIX", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ip_prefix.markdown": "\n\n ### IP_PREFIX\n 截短 IP 至给定的前缀长度。\n\n ```\n row ip4 = to_ip(\"1.2.3.4\"), ip6 = to_ip(\"fe80::cae2:65ff:fece:feb9\")\n | eval ip4_prefix = ip_prefix(ip4, 24, 0), ip6_prefix = ip_prefix(ip6, 0, 112);\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.keep": "KEEP", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.keep.markdown": "### KEEP\n使用 `KEEP` 命令,您可以指定将返回哪些列以及返回这些列的顺序。\n\n要限制返回的列数,请使用列名的逗号分隔列表。将按指定顺序返回这些列:\n \n```\nFROM employees\n| KEEP first_name, last_name, height\n```\n\n您不必按名称指定每个列,而可以使用通配符返回名称匹配某种模式的所有列:\n\n```\nFROM employees\n| KEEP h*\n```\n\n星号通配符 (`*`) 自身将转换为不与其他参数匹配的所有列。此查询将首先返回所有名称以 h 开头的所有列,随后返回所有其他列:\n\n```\nFROM employees\n| KEEP h*, *\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.least": "LEAST", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.least.markdown": "\n\n ### LEAST\n 返回多个列中的最小值。除了可一次对多个列运行以外,此函数与 `MV_MIN` 类似。\n\n ```\n ROW a = 10, b = 20\n | EVAL l = LEAST(a, b)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.left": "LEFT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.left.markdown": "\n\n ### LEFT\n 返回从“字符串”中提取“长度”字符的子字符串,从左侧开始。\n\n ```\n FROM employees\n | KEEP last_name\n | EVAL left = LEFT(last_name, 3)\n | SORT last_name ASC\n | LIMIT 5\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.length": "LENGTH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.length.markdown": "\n\n ### LENGTH\n 返回字符串的字符长度。\n\n ```\n FROM employees\n | KEEP first_name, last_name\n | EVAL fn_length = LENGTH(first_name)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.limit": "LIMIT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.limit.markdown": "### LIMIT\n`LIMIT` 处理命令允许您限制行数:\n \n```\nFROM employees\n| LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.locate": "LOCATE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.locate.markdown": "\n\n ### LOCATE\n 返回一个整数,指示关键字子字符串在另一字符串中的位置\n\n ```\n row a = \"hello\"\n | eval a_ll = locate(a, \"ll\")\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.log": "LOG", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.log.markdown": "\n\n ### LOG\n 以某底数返回值的对数。输入可以为任何数字值,返回值始终为双精度值。\n\n 求零、负数的对数,以及底数为一时将返回 `null`,并显示警告。\n\n ```\n ROW base = 2.0, value = 8.0\n | EVAL s = LOG(base, value)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.log10": "LOG10", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.log10.markdown": "\n\n ### LOG10\n 以底数 10 返回值的对数。输入可以为任何数字值,返回值始终为双精度值。\n\n 求 0 和负数的对数时将返回 `null`,并显示警告。\n\n ```\n ROW d = 1000.0 \n | EVAL s = LOG10(d)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ltrim": "LTRIM", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.ltrim.markdown": "\n\n ### LTRIM\n 从字符串中移除前导空格。\n\n ```\n ROW message = \" some text \", color = \" red \"\n | EVAL message = LTRIM(message)\n | EVAL color = LTRIM(color)\n | EVAL message = CONCAT(\"'\", message, \"'\")\n | EVAL color = CONCAT(\"'\", color, \"'\")\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.markdown": "## ES|QL\n\nES|QL(Elasticsearch 查询语言)查询包含一系列命令,它们用管道字符分隔:`|`。每个查询以**源命令**开头,它会生成一个表,其中通常包含来自 Elasticsearch 的数据。\n\n源命令可后接一个或多个**处理命令**。处理命令可通过添加、移除以及更改行和列来更改前一个命令的输出表。\n\n```\nsource-command\n| processing-command1\n| processing-command2\n```\n\n查询的结果为由最后的处理命令生成的表。 \n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_append": "MV_APPEND", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_append.markdown": "\n\n ### MV_APPEND\n 串联两个多值字段的值。\n\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_avg": "MV_AVG", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_avg.markdown": "\n\n ### MV_AVG\n 将多值字段转换为包含所有值的平均值的单值字段。\n\n ```\n ROW a=[3, 5, 1, 6]\n | EVAL avg_a = MV_AVG(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_concat": "MV_CONCAT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_concat.markdown": "\n\n ### MV_CONCAT\n 将多值字符串表达式转换为单值列,其中包含由分隔符分隔的所有值的串联形式。\n\n ```\n ROW a=[\"foo\", \"zoo\", \"bar\"]\n | EVAL j = MV_CONCAT(a, \", \")\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_count": "MV_COUNT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_count.markdown": "\n\n ### MV_COUNT\n 将多值表达式转换为包含值计数的单值列。\n\n ```\n ROW a=[\"foo\", \"zoo\", \"bar\"]\n | EVAL count_a = MV_COUNT(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_dedupe": "MV_DEDUPE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_dedupe.markdown": "\n\n ### MV_DEDUPE\n 移除多值字段中的重复值。\n\n ```\n ROW a=[\"foo\", \"foo\", \"bar\", \"foo\"]\n | EVAL dedupe_a = MV_DEDUPE(a)\n ```\n 注意:`MV_DEDUPE` 可能但不会始终对列中的值进行排序。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_first": "MV_FIRST", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_first.markdown": "\n\n ### MV_FIRST\n 将多值表达式转换为包含第一个值的\n 单值列。这在从按已知顺序发出多值列的\n 函数(如 `SPLIT`)中读取数据时尤其有用。\n\n 无法保证从底层存储\n 读取多值字段的顺序。它 *通常* 为升序,但不应\n 依赖于此。如果需要最小值,请使用 `MV_MIN` 而不是\n `MV_FIRST`。`MV_MIN` 针对排序值进行了优化,因此\n 对 `MV_FIRST` 没有性能优势。\n\n ```\n ROW a=\"foo;bar;baz\"\n | EVAL first_a = MV_FIRST(SPLIT(a, \";\"))\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_last": "MV_LAST", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_last.markdown": "\n\n ### MV_LAST\n 将多值表达式转换为包含最后一个值的单值\n 列。这在从按已知顺序发出多值列的函数\n (如 `SPLIT`)中读取数据时尤其有用。\n\n 无法保证从底层存储\n 读取多值字段的顺序。它 *通常* 为升序,但不应\n 依赖于此。如果需要最大值,请使用 `MV_MAX` 而不是\n `MV_LAST`。`MV_MAX` 针对排序值进行了优化,因此\n 对 `MV_LAST` 没有性能优势。\n\n ```\n ROW a=\"foo;bar;baz\"\n | EVAL last_a = MV_LAST(SPLIT(a, \";\"))\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_max": "MV_MAX", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_max.markdown": "\n\n ### MV_MAX\n 将多值表达式转换为包含最大值的单值列。\n\n ```\n ROW a=[3, 5, 1]\n | EVAL max_a = MV_MAX(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_median": "MV_MEDIAN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_median.markdown": "\n\n ### MV_MEDIAN\n 将多值字段转换为包含中位数值的单值字段。\n\n ```\n ROW a=[3, 5, 1]\n | EVAL median_a = MV_MEDIAN(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_min": "MV_MIN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_min.markdown": "\n\n ### MV_MIN\n 将多值表达式转换为包含最小值的单值列。\n\n ```\n ROW a=[2, 1]\n | EVAL min_a = MV_MIN(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_slice": "MV_SLICE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_slice.markdown": "\n\n ### MV_SLICE\n 使用起始和结束索引值返回多值字段的子集。\n\n ```\n row a = [1, 2, 2, 3]\n | eval a1 = mv_slice(a, 1), a2 = mv_slice(a, 2, 3)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_sort": "MV_SORT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_sort.markdown": "\n\n ### MV_SORT\n 按字典顺序对多值字段排序。\n\n ```\n ROW a = [4, 2, -3, 2]\n | EVAL sa = mv_sort(a), sd = mv_sort(a, \"DESC\")\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_sum": "MV_SUM", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_sum.markdown": "\n\n ### MV_SUM\n 将多值字段转换为包含所有值的总和的单值字段。\n\n ```\n ROW a=[3, 5, 6]\n | EVAL sum_a = MV_SUM(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_zip": "MV_ZIP", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mv_zip.markdown": "\n\n ### MV_ZIP\n 组合两个使用分隔符联接在一起的多值字段中的值。\n\n ```\n ROW a = [\"x\", \"y\", \"z\"], b = [\"1\", \"2\"]\n | EVAL c = mv_zip(a, b, \"-\")\n | KEEP a, b, c\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mvExpand": "MV_EXPAND", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.mvExpand.markdown": "### MV_EXPAND\n`MV_EXPAND` 处理命令将多值字段扩展成每个值一行,从而复制其他字段: \n```\nROW a=[1,2,3], b=\"b\", j=[\"a\",\"b\"]\n| MV_EXPAND a\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.now": "NOW", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.now.markdown": "\n\n ### NOW\n 返回当前日期和时间。\n\n ```\n ROW current_date = NOW()\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.pi": "PI", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.pi.markdown": "\n\n ### PI\n 返回 Pi,即圆的周长与其直径的比率。\n\n ```\n ROW PI()\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.pow": "POW", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.pow.markdown": "\n\n ### POW\n 返回提升为 `exponent` 幂的 `base` 的值。\n\n ```\n ROW base = 2.0, exponent = 2\n | EVAL result = POW(base, exponent)\n ```\n 注意:此处仍可能使双精度结果溢出;在该情况下,将返回 null。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.predicates": "Null 值", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.predicates.markdown": "### NULL 值\n对于 NULL 比较,请使用 `IS NULL` 和 `IS NOT NULL` 谓词:\n\n```\nFROM employees\n| WHERE birth_date IS NULL\n| KEEP first_name, last_name\n| SORT first_name\n| LIMIT 3\n```\n\n```\nFROM employees\n| WHERE is_rehired IS NOT NULL\n| STATS count(emp_no)\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.rename": "RENAME", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.rename.markdown": "### RENAME\n请使用 `RENAME` 通过以下语法对列重命名:\n\n```\nRENAME AS \n```\n\n例如:\n\n```\nFROM employees\n| KEEP first_name, last_name, still_hired\n| RENAME still_hired AS employed\n```\n\n如果使用新名称的列已存在,将用新列替换该列。\n\n可以使用单个 `RENAME` 命令对多个列重命名:\n\n```\nFROM employees\n| KEEP first_name, last_name\n| RENAME first_name AS fn, last_name AS ln\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.repeat": "REPEAT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.repeat.markdown": "\n\n ### REPEAT\n 返回通过串联 `string` 自身与指定次数 `number` 构造而成的字符串。\n\n ```\n ROW a = \"Hello!\"\n | EVAL triple_a = REPEAT(a, 3);\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.replace": "REPLACE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.replace.markdown": "\n\n ### REPLACE\n 此函数将字符串 `str` 中正则表达式 `regex` 的任何匹配项\n 替换为替代字符串 `newStr`。\n\n ```\n ROW str = \"Hello World\"\n | EVAL str = REPLACE(str, \"World\", \"Universe\")\n | KEEP str\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.right": "RIGHT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.right.markdown": "\n\n ### RIGHT\n 返回从“字符串”中提取“长度”字符的子字符串,从右侧开始。\n\n ```\n FROM employees\n | KEEP last_name\n | EVAL right = RIGHT(last_name, 3)\n | SORT last_name ASC\n | LIMIT 5\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.round": "ROUND", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.round.markdown": "\n\n ### ROUND\n 将数字舍入到指定小数位数。\n 默认值为 0,即返回最近的整数。如果\n 精确度为负数,则将数字舍入到\n 小数点左侧的位数。\n\n ```\n FROM employees\n | KEEP first_name, last_name, height\n | EVAL height_ft = ROUND(height * 3.281, 1)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.row": "ROW", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.row.markdown": "### ROW\n`ROW` 源命令会生成一个行,其中包含一个或多个含有您指定的值的列。这可以用于测试。\n \n```\nROW a = 1, b = \"two\", c = null\n```\n\n请使用方括号创建多值列:\n\n```\nROW a = [2, 1]\n```\n\nROW 支持使用函数:\n\n```\nROW a = ROUND(1.23, 0)\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.rtrim": "RTRIM", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.rtrim.markdown": "\n\n ### RTRIM\n 从字符串中移除尾随空格。\n\n ```\n ROW message = \" some text \", color = \" red \"\n | EVAL message = RTRIM(message)\n | EVAL color = RTRIM(color)\n | EVAL message = CONCAT(\"'\", message, \"'\")\n | EVAL color = CONCAT(\"'\", color, \"'\")\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.show": "SHOW", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.show.markdown": "### SHOW\n`SHOW ` 源命令返回有关部署及其功能的信息:\n\n* 使用 `SHOW INFO` 可返回部署的版本、构建日期和哈希。\n* 使用 `SHOW FUNCTIONS` 可返回所有受支持函数的列表和每个函数的概要。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.signum": "SIGNUM", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.signum.markdown": "\n\n ### SIGNUM\n 返回给定数字的符号。\n 它对负数返回 `-1`,对 `0` 返回 `0`,对正数返回 `1`。\n\n ```\n ROW d = 100.0\n | EVAL s = SIGNUM(d)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sin": "SIN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sin.markdown": "\n\n ### SIN\n 返回角度的正弦三角函数。\n\n ```\n ROW a=1.8 \n | EVAL sin=SIN(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sinh": "SINH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sinh.markdown": "\n\n ### SINH\n 返回角度的双曲正弦。\n\n ```\n ROW a=1.8 \n | EVAL sinh=SINH(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sort": "SORT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sort.markdown": "### SORT\n使用 `SORT` 命令可对一个或多个字段上的行排序:\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height\n```\n\n默认排序顺序为升序。请使用 `ASC` 或 `DESC` 设置显式排序顺序:\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height DESC\n```\n\n如果两个行具有相同的排序键,则保留原始顺序。您可以提供其他排序表达式充当连接断路器:\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height DESC, first_name ASC\n```\n\n#### `null` 值\n默认情况下,会将 `null` 值视为大于任何其他值。使用升序排序顺序时,会最后对 `null` 值排序,而使用降序排序顺序时,会首先对 `null` 值排序。您可以通过提供 `NULLS FIRST` 或 `NULLS LAST` 来更改该排序:\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| SORT first_name ASC NULLS FIRST\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.split": "SPLIT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.split.markdown": "\n\n ### SPLIT\n 将单值字符串拆分成多个字符串。\n\n ```\n ROW words=\"foo;bar;baz;qux;quux;corge\"\n | EVAL word = SPLIT(words, \";\")\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sqrt": "SQRT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.sqrt.markdown": "\n\n ### SQRT\n 返回数字的平方根。输入可以为任何数字值,返回值始终为双精度值。\n 负数和无穷大的平方根为 null。\n\n ```\n ROW d = 100.0\n | EVAL s = SQRT(d)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_contains": "ST_CONTAINS", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_contains.markdown": "\n\n ### ST_CONTAINS\n 返回第一个几何形状是否包含第二个几何形状。\n 这是 `ST_WITHIN` 函数的反向函数。\n\n ```\n FROM airport_city_boundaries\n | WHERE ST_CONTAINS(city_boundary, TO_GEOSHAPE(\"POLYGON((109.35 18.3, 109.45 18.3, 109.45 18.4, 109.35 18.4, 109.35 18.3))\"))\n | KEEP abbrev, airport, region, city, city_location\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_disjoint": "ST_DISJOINT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_disjoint.markdown": "\n\n ### ST_DISJOINT\n 返回两个几何图形或几何图形列是否不相交。\n 这是 `ST_INTERSECTS` 函数的反向函数。\n 从数学上讲:ST_Disjoint(A, B) ⇔ A ⋂ B = ∅\n\n ```\n FROM airport_city_boundaries\n | WHERE ST_DISJOINT(city_boundary, TO_GEOSHAPE(\"POLYGON((-10 -60, 120 -60, 120 60, -10 60, -10 -60))\"))\n | KEEP abbrev, airport, region, city, city_location\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_distance": "ST_DISTANCE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_distance.markdown": "\n\n ### ST_DISTANCE\n 计算两点之间的距离。\n 对于笛卡尔几何形状,这是以相同单位作为原始坐标时的毕达哥拉斯距离。\n 对于地理几何形状而言,这是沿着地球大圆的圆周距离(以米为单位)。\n\n ```\n FROM airports\n | WHERE abbrev == \"CPH\"\n | EVAL distance = ST_DISTANCE(location, city_location)\n | KEEP abbrev, name, location, city_location, distance\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_intersects": "ST_INTERSECTS", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_intersects.markdown": "\n\n ### ST_INTERSECTS\n 如果两个几何形状相交,则返回 true。\n 如果它们有任何共同点,包括其内点\n (沿线的点或多边形内的点),则表示它们相交。\n 这是 `ST_DISJOINT` 函数的反向函数。\n 从数学上讲:ST_Intersects(A, B) ⇔ A ⋂ B ≠ ∅\n\n ```\n FROM airports\n | WHERE ST_INTERSECTS(location, TO_GEOSHAPE(\"POLYGON((42 14, 43 14, 43 15, 42 15, 42 14))\"))\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_within": "ST_WITHIN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_within.markdown": "\n\n ### ST_WITHIN\n 返回第一个几何形状是否在第二个几何形状内。\n 这是 `ST_CONTAINS` 函数的反向函数。\n\n ```\n FROM airport_city_boundaries\n | WHERE ST_WITHIN(city_boundary, TO_GEOSHAPE(\"POLYGON((109.1 18.15, 109.6 18.15, 109.6 18.65, 109.1 18.65, 109.1 18.15))\"))\n | KEEP abbrev, airport, region, city, city_location\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_x": "ST_X", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_x.markdown": "\n\n ### ST_X\n 从提供的点中提取 `x` 坐标。\n 如果点的类型为 `geo_point`,则这等同于提取 `longitude` 值。\n\n ```\n ROW point = TO_GEOPOINT(\"POINT(42.97109629958868 14.7552534006536)\")\n | EVAL x = ST_X(point), y = ST_Y(point)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_y": "ST_Y", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.st_y.markdown": "\n\n ### ST_Y\n 从提供的点中提取 `y` 坐标。\n 如果点的类型为 `geo_point`,则这等同于提取 `latitude` 值。\n\n ```\n ROW point = TO_GEOPOINT(\"POINT(42.97109629958868 14.7552534006536)\")\n | EVAL x = ST_X(point), y = ST_Y(point)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.starts_with": "STARTS_WITH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.starts_with.markdown": "\n\n ### STARTS_WITH\n 返回指示关键字字符串是否以另一个字符串开头的布尔值。\n\n ```\n FROM employees\n | KEEP last_name\n | EVAL ln_S = STARTS_WITH(last_name, \"B\")\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.statsby": "STATS ...BY", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.statsby.markdown": "### STATS ...BY\n使用 `STATS ...BY` 可根据公共值对行分组,并计算已分组行中的一个或多个聚合值。\n\n**示例**:\n\n```\nFROM employees\n| STATS count = COUNT(emp_no) BY languages\n| SORT languages\n```\n\n如果省略 `BY`,输出表实际将包含一行,其中为应用于整个数据集的聚合:\n\n```\nFROM employees\n| STATS avg_lang = AVG(languages)\n```\n\n可以计算多个值:\n\n```\nFROM employees\n| STATS avg_lang = AVG(languages), max_lang = MAX(languages)\n```\n\n也可以按多个值分组(仅长整型和关键字家族字段支持):\n\n```\nFROM employees\n| EVAL hired = DATE_FORMAT(hire_date, \"YYYY\")\n| STATS avg_salary = AVG(salary) BY hired, languages.long\n| EVAL avg_salary = ROUND(avg_salary)\n| SORT hired, languages.long\n```\n\n请参阅**聚合函数**获取可与 `STATS ...BY` 搭配使用的函数列表。\n\n聚合函数和分组表达式均接受其他函数。这在对多值列使用 `STATS...BY` 时有用。例如,要计算平均工资变动,可以首先使用 `MV_AVG` 对每名员工的多个值求平均值,然后将结果用于 `AVG` 函数:\n\n```\nFROM employees\n| STATS avg_salary_change = AVG(MV_AVG(salary_change))\n```\n\n按表达式分组的示例为根据员工姓氏的第一个字母对其进行分组:\n\n```\nFROM employees\n| STATS my_count = COUNT() BY LEFT(last_name, 1)\n| SORT `LEFT(last_name, 1)`\n```\n\n指定输出列名称为可选操作。如果未指定,新列名称等于该表达式。以下查询将返回名为 `AVG(salary)` 的列:\n\n```\nFROM employees\n| STATS AVG(salary)\n```\n\n由于此名称包含特殊字符,在后续命令中使用该名称时,需要用反撇号 (`) 引用它:\n\n```\nFROM employees\n| STATS AVG(salary)\n| EVAL avg_salary_rounded = ROUND(`AVG(salary)`)\n```\n\n**注意**:不包含任何组的 `STATS` 比添加组更快。\n\n**注意**:当前,根据单一表达式进行分组比根据许多表达式进行分组更为优化。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.stringOperators": "LIKE 和 RLIKE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.stringOperators.markdown": "### LIKE 和 RLIKE\n使用通配符或正则表达式比较字符串时,请使用 `LIKE` 或 `RLIKE`:\n\n使用 `LIKE` 时,可使用通配符来匹配字符串。支持以下通配符字符:\n\n* `*` 匹配零个或更多字符。\n* `?` 匹配一个字符。\n\n```\nFROM employees\n| WHERE first_name LIKE \"?b*\"\n| KEEP first_name, last_name\n```\n\n使用 `RLIKE` 时,可使用正则表达式来匹配字符串:\n\n```\nFROM employees\n| WHERE first_name RLIKE \".leja.*\"\n| KEEP first_name, last_name\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.substring": "SUBSTRING", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.substring.markdown": "\n\n ### SUBSTRING\n 返回字符串的子字符串,用起始位置和可选长度指定\n\n ```\n FROM employees\n | KEEP last_name\n | EVAL ln_sub = SUBSTRING(last_name, 1, 3)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.tan": "TAN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.tan.markdown": "\n\n ### TAN\n 返回角度的正切三角函数。\n\n ```\n ROW a=1.8 \n | EVAL tan=TAN(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.tanh": "TANH", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.tanh.markdown": "\n\n ### TANH\n 返回角度的双曲正切函数。\n\n ```\n ROW a=1.8 \n | EVAL tanh=TANH(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.tau": "TAU", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.tau.markdown": "\n\n ### TAU\n 返回圆的圆周长与其半径的比率。\n\n ```\n ROW TAU()\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_base64": "TO_BASE64", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_base64.markdown": "\n\n ### TO_BASE64\n 将字符串编码为 base64 字符串。\n\n ```\n row a = \"elastic\" \n | eval e = to_base64(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_boolean": "TO_BOOLEAN", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_boolean.markdown": "\n\n ### TO_BOOLEAN\n 将输入值转换为布尔值。\n 字符串值 *true* 将不区分大小写并被转换为布尔值 *true*。\n 对于任何其他值,包括空字符串,此函数将返回 *false*。\n 数字值 *0* 将转换为 *false*,任何其他值将转换为 *true*。\n\n ```\n ROW str = [\"true\", \"TRuE\", \"false\", \"\", \"yes\", \"1\"]\n | EVAL bool = TO_BOOLEAN(str)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_cartesianpoint": "TO_CARTESIANPOINT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_cartesianpoint.markdown": "\n\n ### TO_CARTESIANPOINT\n 将输入值转换为 `cartesian_point` 值。\n 字符串只有符合 WKT 点格式时,才能成功转换。\n\n ```\n ROW wkt = [\"POINT(4297.11 -1475.53)\", \"POINT(7580.93 2272.77)\"]\n | MV_EXPAND wkt\n | EVAL pt = TO_CARTESIANPOINT(wkt)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_cartesianshape": "TO_CARTESIANSHAPE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_cartesianshape.markdown": "\n\n ### TO_CARTESIANSHAPE\n 将输入值转换为 `cartesian_shape` 值。\n 字符串只有符合 WKT 格式时,才能成功转换。\n\n ```\n ROW wkt = [\"POINT(4297.11 -1475.53)\", \"POLYGON ((3339584.72 1118889.97, 4452779.63 4865942.27, 2226389.81 4865942.27, 1113194.90 2273030.92, 3339584.72 1118889.97))\"]\n | MV_EXPAND wkt\n | EVAL geom = TO_CARTESIANSHAPE(wkt)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_datetime": "TO_DATETIME", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_datetime.markdown": "\n\n ### TO_DATETIME\n 将输入值转换为日期值。\n 仅当字符串采用 `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` 格式时,才可进行成功转换。\n 要转换其他格式的日期,请使用 `DATE_PARSE`。\n\n ```\n ROW string = [\"1953-09-02T00:00:00.000Z\", \"1964-06-02T00:00:00.000Z\", \"1964-06-02 00:00:00\"]\n | EVAL datetime = TO_DATETIME(string)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_degrees": "TO_DEGREES", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_degrees.markdown": "\n\n ### TO_DEGREES\n 将弧度转换为度数。\n\n ```\n ROW rad = [1.57, 3.14, 4.71]\n | EVAL deg = TO_DEGREES(rad)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_double": "TO_DOUBLE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_double.markdown": "\n\n ### TO_DOUBLE\n 将输入值转换为双精度值。如果输入参数为日期类型,\n 会将其值解析为自 Unix epoch 以来的毫秒数,\n 并转换为双精度值。布尔值 *true* 将转换为双精度值 *1.0*,*false* 转换为 *0.0*。\n\n ```\n ROW str1 = \"5.20128E11\", str2 = \"foo\"\n | EVAL dbl = TO_DOUBLE(\"520128000000\"), dbl1 = TO_DOUBLE(str1), dbl2 = TO_DOUBLE(str2)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_geopoint": "TO_GEOPOINT", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_geopoint.markdown": "\n\n ### TO_GEOPOINT\n 将输入值转换为 `geo_point` 值。\n 字符串只有符合 WKT 点格式时,才能成功转换。\n\n ```\n ROW wkt = \"POINT(42.97109630194 14.7552534413725)\"\n | EVAL pt = TO_GEOPOINT(wkt)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_geoshape": "TO_GEOSHAPE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_geoshape.markdown": "\n\n ### TO_GEOSHAPE\n 将输入值转换为 `geo_shape` 值。\n 字符串只有符合 WKT 格式时,才能成功转换。\n\n ```\n ROW wkt = \"POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))\"\n | EVAL geom = TO_GEOSHAPE(wkt)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_integer": "TO_INTEGER", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_integer.markdown": "\n\n ### TO_INTEGER\n 将输入值转换为整数值。\n 如果输入参数为日期类型,会将其值解析为自 Unix epoch 以来\n 的毫秒数,并转换为整数。\n 布尔值 *true* 将转换为整数 *1*,*false* 转换为 *0*。\n\n ```\n ROW long = [5013792, 2147483647, 501379200000]\n | EVAL int = TO_INTEGER(long)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_ip": "TO_IP", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_ip.markdown": "\n\n ### TO_IP\n 将输入字符串转换为 IP 值。\n\n ```\n ROW str1 = \"1.1.1.1\", str2 = \"foo\"\n | EVAL ip1 = TO_IP(str1), ip2 = TO_IP(str2)\n | WHERE CIDR_MATCH(ip1, \"1.0.0.0/8\")\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_long": "TO_LONG", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_long.markdown": "\n\n ### TO_LONG\n 将输入值转换为长整型值。如果输入参数为日期类型,\n 会将其值解析为自 Unix epoch 以来的毫秒数,并转换为长整型值。\n 布尔值 *true* 将转换为长整型值 *1*,*false* 转换为 *0*。\n\n ```\n ROW str1 = \"2147483648\", str2 = \"2147483648.2\", str3 = \"foo\"\n | EVAL long1 = TO_LONG(str1), long2 = TO_LONG(str2), long3 = TO_LONG(str3)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_lower": "TO_LOWER", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_lower.markdown": "\n\n ### TO_LOWER\n 返回一个新字符串,表示已将输入字符串转为小写。\n\n ```\n ROW message = \"Some Text\"\n | EVAL message_lower = TO_LOWER(message)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_radians": "TO_RADIANS", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_radians.markdown": "\n\n ### TO_RADIANS\n 将度数转换为弧度。\n\n ```\n ROW deg = [90.0, 180.0, 270.0]\n | EVAL rad = TO_RADIANS(deg)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_string": "TO_STRING", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_string.markdown": "\n\n ### TO_STRING\n 将输入值转换为字符串。\n\n ```\n ROW a=10\n | EVAL j = TO_STRING(a)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_unsigned_long": "TO_UNSIGNED_LONG", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_unsigned_long.markdown": "\n\n ### TO_UNSIGNED_LONG\n 将输入值转换为无符号长整型值。如果输入参数为日期类型,\n 会将其值解析为自 Unix epoch 以来的毫秒数,并转换为无符号长整型值。\n 布尔值 *true* 将转换为无符号长整型值 *1*,*false* 转换为 *0*。\n\n ```\n ROW str1 = \"2147483648\", str2 = \"2147483648.2\", str3 = \"foo\"\n | EVAL long1 = TO_UNSIGNED_LONG(str1), long2 = TO_ULONG(str2), long3 = TO_UL(str3)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_upper": "TO_UPPER", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_upper.markdown": "\n\n ### TO_UPPER\n 返回一个新字符串,表示已将输入字符串转为大写。\n\n ```\n ROW message = \"Some Text\"\n | EVAL message_upper = TO_UPPER(message)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_version": "TO_VERSION", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.to_version.markdown": "\n\n ### TO_VERSION\n 将输入字符串转换为版本值。\n\n ```\n ROW v = TO_VERSION(\"1.2.3\")\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.trim": "TRIM", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.trim.markdown": "\n\n ### TRIM\n 从字符串中移除前导和尾随空格。\n\n ```\n ROW message = \" some text \", color = \" red \"\n | EVAL message = TRIM(message)\n | EVAL color = TRIM(color)\n ```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.where": "WHERE", - "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.where.markdown": "### WHERE\n使用 `WHERE` 可生成一个表,其中包含输入表中所提供的条件评估为 `true` 的所有行:\n \n```\nFROM employees\n| KEEP first_name, last_name, still_hired\n| WHERE still_hired == true\n```\n\n#### 运算符\n\n请参阅**运算符**了解所支持的运算符的概览。\n\n#### 函数\n`WHERE` 支持各种用于计算值的函数。请参阅**函数**了解更多信息。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentationLabel": "文档", + "languageDocumentationPopover.documentationESQL.abs": "ABS", + "languageDocumentationPopover.documentationESQL.abs.markdown": "\n\n ### ABS\n 返回绝对值。\n\n ```\n ROW number = -1.0 \n | EVAL abs_number = ABS(number)\n ```\n ", + "languageDocumentationPopover.documentationESQL.acos": "ACOS", + "languageDocumentationPopover.documentationESQL.acos.markdown": "\n\n ### ACOS\n 返回 `n` 的反余弦作为角度,以弧度表示。\n\n ```\n ROW a=.9\n | EVAL acos=ACOS(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.asin": "ASIN", + "languageDocumentationPopover.documentationESQL.asin.markdown": "\n\n ### ASIN\n 返回输入数字表达式的反正弦\n 作为角度,以弧度表示。\n\n ```\n ROW a=.9\n | EVAL asin=ASIN(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.atan": "ATAN", + "languageDocumentationPopover.documentationESQL.atan.markdown": "\n\n ### ATAN\n 返回输入数字表达式的反正切\n 作为角度,以弧度表示。\n\n ```\n ROW a=12.9\n | EVAL atan=ATAN(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.atan2": "ATAN2", + "languageDocumentationPopover.documentationESQL.atan2.markdown": "\n\n ### ATAN2\n 笛卡儿平面中正 x 轴\n 与从原点到点 (x , y) 构成的射线之间的角度,以弧度表示。\n\n ```\n ROW y=12.9, x=.6\n | EVAL atan2=ATAN2(y, x)\n ```\n ", + "languageDocumentationPopover.documentationESQL.autoBucketFunction": "BUCKET", + "languageDocumentationPopover.documentationESQL.autoBucketFunction.markdown": "### BUCKET\n用日期时间或数字输入创建值(存储桶)的分组。存储桶的大小可以直接提供,或基于建议的计数和值范围进行选择。\n\n`BUCKET` 以两种模式运行:\n\n1.在此模式下基于存储桶计数建议(四个参数)和范围计算存储桶的大小。\n2.在此模式下直接提供存储桶大小(两个参数)。\n\n使用存储桶的目标数量、起始范围和结束范围,`BUCKET` 将选取适当的存储桶大小以生成目标数量或更小数量的存储桶。\n\n例如,一年请求多达 20 个存储桶会按每月时间间隔组织数据:\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hire_date = MV_SORT(VALUES(hire_date)) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT hire_date\n```\n\n**注意**:目标并不是提供存储桶的确切目标数量,而是选择一个范围,最多提供存储桶的目标数量。\n\n可以组合 `BUCKET` 与聚合以创建直方图:\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_month = COUNT(*) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT month\n```\n\n**注意**:`BUCKET` 不会创建未匹配任何文档的存储桶。因此,上一示例缺少 `1985-03-01` 和其他日期。\n\n如果需要更多存储桶,可能导致更小的范围。例如,如果一年内最多请求 100 个存储桶,会导致周期为周的存储桶:\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_week = COUNT(*) BY week = BUCKET(hire_date, 100, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT week\n```\n\n**注意**:`AUTO_BUCKET` 不筛选任何行。它只会使用提供的范围来选取适当的存储桶大小。对于值超出范围的行,它会返回与超出范围的存储桶对应的存储桶值。组合 `BUCKET` 与 `WHERE` 可筛选行。\n\n如果提前已知所需存储桶大小,则只需提供它作为第二个参数,而忽略范围:\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS hires_per_week = COUNT(*) BY week = BUCKET(hire_date, 1 week)\n| SORT week\n```\n\n**注意**:提供存储桶大小作为第二个参数时,它必须为持续时间或日期期间。\n\n`BUCKET` 还可对数字字段执行操作。例如,要创建工资直方图:\n\n```\nFROM employees\n| STATS COUNT(*) by bs = BUCKET(salary, 20, 25324, 74999)\n| SORT bs\n```\n\n与前面的有意筛选日期范围示例不同,您极少想要筛选数值范围。您必须分别查找最小值和最大值。ES|QL 尚未提供简便方法来自动执行此操作。\n\n如果提前已知所需存储桶大小,则可以忽略该范围。只需提供它作为第二个参数即可:\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS c = COUNT(1) BY b = BUCKET(salary, 5000.)\n| SORT b\n```\n\n**注意**:提供存储桶大小作为第二个参数时,它必须为 **浮点类型**。\n\n这里提供了一个示例,用于为过去 24 小时创建小时存储桶,并计算每小时的事件数:\n\n```\nFROM sample_data\n| WHERE @timestamp >= NOW() - 1 day and @timestamp < NOW()\n| STATS COUNT(*) BY bucket = BUCKET(@timestamp, 25, NOW() - 1 day, NOW())\n```\n\n这里提供了一个示例,用于为 1985 年创建月度存储桶,并按聘用月份计算平均工资:\n\n```\nFROM employees\n| WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n| STATS AVG(salary) BY bucket = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n| SORT bucket\n```\n\n`BUCKET` 可用在 `STATS …​ BY …`​ 命令的聚合和分组部分, 前提是在聚合部分中,该函数 **由在分组部分中定义的别名引用**,或使用完全相同的表达式调用。\n\n例如:\n\n```\nFROM employees\n| STATS s1 = b1 + 1, s2 = BUCKET(salary / 1000 + 999, 50.) + 2 BY b1 = BUCKET(salary / 100 + 99, 50.), b2 = BUCKET(salary / 1000 + 999, 50.)\n| SORT b1, b2\n| KEEP s1, b1, s2, b2\n```\n ", + "languageDocumentationPopover.documentationESQL.binaryOperators": "二进制运算符", + "languageDocumentationPopover.documentationESQL.binaryOperators.markdown": "### 二进制运算符\n支持这些二进制比较运算符:\n\n* 等于:`==`\n* 不等于:`!=`\n* 小于:`<`\n* 小于或等于:`<=`\n* 大于:`>`\n* 大于或等于:`>=`\n* 加:`+`\n* 减:`-`\n* 乘:`*`\n* 除:`/`\n* 取模:`%`\n ", + "languageDocumentationPopover.documentationESQL.booleanOperators": "布尔运算符", + "languageDocumentationPopover.documentationESQL.booleanOperators.markdown": "### 布尔运算符\n支持以下布尔运算符:\n\n* `AND`\n* `OR`\n* `NOT`\n ", + "languageDocumentationPopover.documentationESQL.bucket": "BUCKET", + "languageDocumentationPopover.documentationESQL.bucket.markdown": "\n\n ### BUCKET\n 用日期时间或数字输入创建值(存储桶)的分组。\n 存储桶的大小可以直接提供,或基于建议的计数和值范围进行选择。\n\n ```\n FROM employees\n | WHERE hire_date >= \"1985-01-01T00:00:00Z\" AND hire_date < \"1986-01-01T00:00:00Z\"\n | STATS hire_date = MV_SORT(VALUES(hire_date)) BY month = BUCKET(hire_date, 20, \"1985-01-01T00:00:00Z\", \"1986-01-01T00:00:00Z\")\n | SORT hire_date\n ```\n ", + "languageDocumentationPopover.documentationESQL.case": "CASE", + "languageDocumentationPopover.documentationESQL.case.markdown": "\n\n ### CASE\n 接受成对的条件和值。此函数返回属于第一个\n 评估为 `true` 的条件的值。\n\n 如果参数数量为奇数,则最后一个参数为\n 在无条件匹配时返回的默认值。如果参数数量为偶数,且\n 无任何条件匹配,则此函数返回 `null`。\n\n ```\n FROM employees\n | EVAL type = CASE(\n languages <= 1, \"monolingual\",\n languages <= 2, \"bilingual\",\n \"polyglot\")\n | KEEP emp_no, languages, type\n ```\n ", + "languageDocumentationPopover.documentationESQL.castOperator": "Cast (::)", + "languageDocumentationPopover.documentationESQL.castOperator.markdown": "### CAST (`::`)\n`::` 运算符为 `TO_` 类型转换函数提供了实用的替代语法。\n\n例如:\n```\nROW ver = CONCAT((\"0\"::INT + 1)::STRING, \".2.3\")::VERSION\n```\n ", + "languageDocumentationPopover.documentationESQL.cbrt": "CBRT", + "languageDocumentationPopover.documentationESQL.cbrt.markdown": "\n\n ### CBRT\n 返回数字的立方根。输入可以为任何数字值,返回值始终为双精度值。\n 无穷大的立方根为 null。\n\n ```\n ROW d = 1000.0\n | EVAL c = cbrt(d)\n ```\n ", + "languageDocumentationPopover.documentationESQL.ceil": "CEIL", + "languageDocumentationPopover.documentationESQL.ceil.markdown": "\n\n ### CEIL\n 将数字四舍五入为最近的整数。\n\n ```\n ROW a=1.8\n | EVAL a=CEIL(a)\n ```\n 注意:对于 `long`(包括无符号值)和 `integer`,这相当于“无操作”。对于 `double`,这会提取最接近整数的 `double` 值,类似于 Math.ceil。\n ", + "languageDocumentationPopover.documentationESQL.cidr_match": "CIDR_MATCH", + "languageDocumentationPopover.documentationESQL.cidr_match.markdown": "\n\n ### CIDR_MATCH\n 如果提供的 IP 包含在所提供的其中一个 CIDR 块中,则返回 true。\n\n ```\n FROM hosts \n | WHERE CIDR_MATCH(ip1, \"127.0.0.2/32\", \"127.0.0.3/32\") \n | KEEP card, host, ip0, ip1\n ```\n ", + "languageDocumentationPopover.documentationESQL.coalesce": "COALESCE", + "languageDocumentationPopover.documentationESQL.coalesce.markdown": "\n\n ### COALESCE\n 返回它的第一个不为 null 的参数。如果所有参数均为 null,则返回 `null`。\n\n ```\n ROW a=null, b=\"b\"\n | EVAL COALESCE(a, b)\n ```\n ", + "languageDocumentationPopover.documentationESQL.concat": "CONCAT", + "languageDocumentationPopover.documentationESQL.concat.markdown": "\n\n ### CONCAT\n 串联两个或多个字符串。\n\n ```\n FROM employees\n | KEEP first_name, last_name\n | EVAL fullname = CONCAT(first_name, \" \", last_name)\n ```\n ", + "languageDocumentationPopover.documentationESQL.cos": "COS", + "languageDocumentationPopover.documentationESQL.cos.markdown": "\n\n ### COS\n 返回角度的余弦。\n\n ```\n ROW a=1.8 \n | EVAL cos=COS(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.cosh": "COSH", + "languageDocumentationPopover.documentationESQL.cosh.markdown": "\n\n ### COSH\n 返回角度的双曲余弦。\n\n ```\n ROW a=1.8 \n | EVAL cosh=COSH(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.date_diff": "DATE_DIFF", + "languageDocumentationPopover.documentationESQL.date_diff.markdown": "\n\n ### DATE_DIFF\n 从 `endTimestamp` 中减去 `startTimestamp`,并以倍数 `unit` 返回差异。\n 如果 `startTimestamp` 晚于 `endTimestamp`,则返回负值。\n\n ```\n ROW date1 = TO_DATETIME(\"2023-12-02T11:00:00.000Z\"), date2 = TO_DATETIME(\"2023-12-02T11:00:00.001Z\")\n | EVAL dd_ms = DATE_DIFF(\"microseconds\", date1, date2)\n ```\n ", + "languageDocumentationPopover.documentationESQL.date_extract": "DATE_EXTRACT", + "languageDocumentationPopover.documentationESQL.date_extract.markdown": "\n\n ### DATE_EXTRACT\n 提取日期的某些部分,如年、月、日、小时。\n\n ```\n ROW date = DATE_PARSE(\"yyyy-MM-dd\", \"2022-05-06\")\n | EVAL year = DATE_EXTRACT(\"year\", date)\n ```\n ", + "languageDocumentationPopover.documentationESQL.date_format": "DATE_FORMAT", + "languageDocumentationPopover.documentationESQL.date_format.markdown": "\n\n ### DATE_FORMAT\n 以提供的格式返回日期的字符串表示形式。\n\n ```\n FROM employees\n | KEEP first_name, last_name, hire_date\n | EVAL hired = DATE_FORMAT(\"YYYY-MM-dd\", hire_date)\n ```\n ", + "languageDocumentationPopover.documentationESQL.date_parse": "DATE_PARSE", + "languageDocumentationPopover.documentationESQL.date_parse.markdown": "\n\n ### DATE_PARSE\n 通过使用在第一个参数中指定的格式来解析第二个参数,从而返回日期。\n\n ```\n ROW date_string = \"2022-05-06\"\n | EVAL date = DATE_PARSE(\"yyyy-MM-dd\", date_string)\n ```\n ", + "languageDocumentationPopover.documentationESQL.date_trunc": "DATE_TRUNC", + "languageDocumentationPopover.documentationESQL.date_trunc.markdown": "\n\n ### DATE_TRUNC\n 将日期向下舍入到最近的时间间隔。\n\n ```\n FROM employees\n | KEEP first_name, last_name, hire_date\n | EVAL year_hired = DATE_TRUNC(1 year, hire_date)\n ```\n ", + "languageDocumentationPopover.documentationESQL.dissect": "DISSECT", + "languageDocumentationPopover.documentationESQL.dissect.markdown": "### DISSECT\n使用 `DISSECT`,您可以从字符串中提取结构化数据。`DISSECT` 将根据基于分隔符的模式来匹配字符串,并提取指定键作为列。\n\n请参阅[分解处理器文档](https://www.elastic.co/guide/en/elasticsearch/reference/current/dissect-processor.html)了解分解模式的语法。\n\n```\nROW a = \"1953-01-23T12:15:00Z - some text - 127.0.0.1\"\n| DISSECT a \"%'{Y}-%{M}-%{D}T%{h}:%{m}:%{s}Z - %{msg} - %{ip}'\"\n``` ", + "languageDocumentationPopover.documentationESQL.drop": "DROP", + "languageDocumentationPopover.documentationESQL.drop.markdown": "### DROP\n使用 `DROP` 可从表中移除列:\n \n```\nFROM employees\n| DROP height\n```\n\n您不必按名称指定每个列,而可以使用通配符丢弃名称匹配某种模式的所有列:\n\n```\nFROM employees\n| DROP height*\n```\n ", + "languageDocumentationPopover.documentationESQL.e": "E", + "languageDocumentationPopover.documentationESQL.e.markdown": "\n\n ### E\n 返回 Euler 函数的编号。\n\n ```\n ROW E()\n ```\n ", + "languageDocumentationPopover.documentationESQL.ends_with": "ENDS_WITH", + "languageDocumentationPopover.documentationESQL.ends_with.markdown": "\n\n ### ENDS_WITH\n 返回布尔值,指示关键字字符串是否以另一个字符串结尾。\n\n ```\n FROM employees\n | KEEP last_name\n | EVAL ln_E = ENDS_WITH(last_name, \"d\")\n ```\n ", + "languageDocumentationPopover.documentationESQL.enrich": "ENRICH", + "languageDocumentationPopover.documentationESQL.enrich.markdown": "### ENRICH\n您可以使用 `ENRICH` 将来自现有索引的数据添加到传入记录中。它类似于[采集扩充](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html),但作用于查询时间。\n\n```\nROW language_code = \"1\"\n| ENRICH languages_policy\n```\n\n执行 `ENRICH` 需要[扩充策略](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html#enrich-policy)。扩充策略定义一个匹配字段(键字段)和一组扩充字段。\n\n`ENRICH` 将根据匹配字段值在[扩充索引](https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest-enriching-data.html#enrich-index)中查找记录。输入数据集中的匹配键可以使用 `ON ` 来定义;如果未指定,将对字段名称与在扩充策略中定义的匹配字段相同的字段执行匹配。\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a\n```\n\n您可以使用 `WITH , ...` 语法指定必须将哪些属性(在那些在策略中定义为扩充字段的字段之间)添加到结果中。\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a WITH language_name\n```\n\n还可以使用 `WITH new_name=` 重命名属性\n\n```\nROW a = \"1\"\n| ENRICH languages_policy ON a WITH name = language_name\n```\n\n默认情况下(如果未定义任何 `WITH`),`ENRICH` 会将在扩充策略中定义的所有扩充字段添加到结果中。\n\n如果出现名称冲突,新创建的字段将覆盖现有字段。\n ", + "languageDocumentationPopover.documentationESQL.eval": "EVAL", + "languageDocumentationPopover.documentationESQL.eval.markdown": "### EVAL\n`EVAL` 允许您添加新列:\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| EVAL height_feet = height * 3.281, height_cm = height * 100\n```\n\n如果指定列已存在,将丢弃现有列,并将新列追加到表后面:\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| EVAL height = height * 3.281\n```\n\n#### 函数\n`EVAL` 支持各种用于计算值的函数。请参阅“函数”了解更多信息。\n ", + "languageDocumentationPopover.documentationESQL.floor": "FLOOR", + "languageDocumentationPopover.documentationESQL.floor.markdown": "\n\n ### FLOOR\n 将数字向下舍入到最近的整数。\n\n ```\n ROW a=1.8\n | EVAL a=FLOOR(a)\n ```\n 注意:对于 `long`(包括无符号值)和 `integer`,这相当于“无操作”。\n 对于 `double`,这会提取最接近整数的 `double` 值,\n 类似于 Math.floor。\n ", + "languageDocumentationPopover.documentationESQL.from": "FROM", + "languageDocumentationPopover.documentationESQL.from_base64": "FROM_BASE64", + "languageDocumentationPopover.documentationESQL.from_base64.markdown": "\n\n ### FROM_BASE64\n 解码 base64 字符串。\n\n ```\n row a = \"ZWxhc3RpYw==\" \n | eval d = from_base64(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.from.markdown": "### FROM\n`FROM` 源命令返回一个表,其中最多包含 10,000 个来自数据流、索引或别名的文档。生成的表中的每一行代表一个文档。每一列对应一个字段,并可以通过该字段的名称进行访问。\n\n```\nFROM employees\n```\n\n您可以使用[日期数学表达式](https://www.elastic.co/guide/en/elasticsearch/reference/current/api-conventions.html#api-date-math-index-names)来引用索引、别名和数据流。这可能对时间序列数据非常有用。\n\n使用逗号分隔列表或通配符可查询多个数据流、索引或别名:\n\n```\nFROM employees-00001,employees-*\n```\n\n#### 元数据\n\nES|QL 可访问以下元数据字段:\n\n* `_index`:文档所属的索引。字段类型为 `keyword`.\n* `_id`:源文档的 ID。字段类型为 `keyword`.\n* `_version`:源文档的版本。字段类型为 `long`。\n\n使用 `METADATA` 指令可启用元数据字段:\n\n```\nFROM index [METADATA _index, _id]\n```\n\n元数据字段仅在数据源为索引时可用。因此,`FROM` 是唯一支持 `METADATA` 指令的源命令。\n\n启用后,这些字段将可用于后续处理命令,就像其他索引字段一样:\n\n```\nFROM ul_logs, apps [METADATA _index, _version]\n| WHERE id IN (13, 14) AND _version == 1\n| EVAL key = CONCAT(_index, \"_\", TO_STR(id))\n| SORT id, _index\n| KEEP id, _index, _version, key\n```\n\n此外,与索引字段类似,一旦执行了聚合,后续命令将无法再访问元数据字段,除非它用作分组字段:\n\n```\nFROM employees [METADATA _index, _id]\n| STATS max = MAX(emp_no) BY _index\n```\n ", + "languageDocumentationPopover.documentationESQL.greatest": "GREATEST", + "languageDocumentationPopover.documentationESQL.greatest.markdown": "\n\n ### GREATEST\n 返回多个列中的最大值。除了可一次对多个列运行以外,\n 此函数与 `MV_MAX` 类似。\n\n ```\n ROW a = 10, b = 20\n | EVAL g = GREATEST(a, b)\n ```\n 注意:对 `keyword` 或 `text` 字段运行时,此函数将按字母顺序返回最后一个字符串。对 `boolean` 列运行时,如果任何值为 `true`,此函数将返回 `true`。\n ", + "languageDocumentationPopover.documentationESQL.grok": "GROK", + "languageDocumentationPopover.documentationESQL.grok.markdown": "### GROK\n使用 `GROK`,您可以从字符串中提取结构化数据。`GROK` 将基于正则表达式根据模式来匹配字符串,并提取指定模式作为列。\n\n请参阅 [grok 处理器文档](https://www.elastic.co/guide/en/elasticsearch/reference/current/grok-processor.html)了解 grok 模式的语法。\n\n```\nROW a = \"12 15.5 15.6 true\"\n| GROK a \"%'{NUMBER:b:int}' %'{NUMBER:c:float}' %'{NUMBER:d:double}' %'{WORD:e:boolean}'\"\n```\n ", + "languageDocumentationPopover.documentationESQL.inOperator": "IN", + "languageDocumentationPopover.documentationESQL.inOperator.markdown": "### IN\n`IN` 运算符允许测试字段或表达式是否等于文本、字段或表达式列表中的元素:\n\n```\nROW a = 1, b = 4, c = 3\n| WHERE c-a IN (3, b / 2, a)\n```\n ", + "languageDocumentationPopover.documentationESQL.ip_prefix": "IP_PREFIX", + "languageDocumentationPopover.documentationESQL.ip_prefix.markdown": "\n\n ### IP_PREFIX\n 截短 IP 至给定的前缀长度。\n\n ```\n row ip4 = to_ip(\"1.2.3.4\"), ip6 = to_ip(\"fe80::cae2:65ff:fece:feb9\")\n | eval ip4_prefix = ip_prefix(ip4, 24, 0), ip6_prefix = ip_prefix(ip6, 0, 112);\n ```\n ", + "languageDocumentationPopover.documentationESQL.keep": "KEEP", + "languageDocumentationPopover.documentationESQL.keep.markdown": "### KEEP\n使用 `KEEP` 命令,您可以指定将返回哪些列以及返回这些列的顺序。\n\n要限制返回的列数,请使用列名的逗号分隔列表。将按指定顺序返回这些列:\n \n```\nFROM employees\n| KEEP first_name, last_name, height\n```\n\n您不必按名称指定每个列,而可以使用通配符返回名称匹配某种模式的所有列:\n\n```\nFROM employees\n| KEEP h*\n```\n\n星号通配符 (`*`) 自身将转换为不与其他参数匹配的所有列。此查询将首先返回所有名称以 h 开头的所有列,随后返回所有其他列:\n\n```\nFROM employees\n| KEEP h*, *\n```\n ", + "languageDocumentationPopover.documentationESQL.least": "LEAST", + "languageDocumentationPopover.documentationESQL.least.markdown": "\n\n ### LEAST\n 返回多个列中的最小值。除了可一次对多个列运行以外,此函数与 `MV_MIN` 类似。\n\n ```\n ROW a = 10, b = 20\n | EVAL l = LEAST(a, b)\n ```\n ", + "languageDocumentationPopover.documentationESQL.left": "LEFT", + "languageDocumentationPopover.documentationESQL.left.markdown": "\n\n ### LEFT\n 返回从“字符串”中提取“长度”字符的子字符串,从左侧开始。\n\n ```\n FROM employees\n | KEEP last_name\n | EVAL left = LEFT(last_name, 3)\n | SORT last_name ASC\n | LIMIT 5\n ```\n ", + "languageDocumentationPopover.documentationESQL.length": "LENGTH", + "languageDocumentationPopover.documentationESQL.length.markdown": "\n\n ### LENGTH\n 返回字符串的字符长度。\n\n ```\n FROM employees\n | KEEP first_name, last_name\n | EVAL fn_length = LENGTH(first_name)\n ```\n ", + "languageDocumentationPopover.documentationESQL.limit": "LIMIT", + "languageDocumentationPopover.documentationESQL.limit.markdown": "### LIMIT\n`LIMIT` 处理命令允许您限制行数:\n \n```\nFROM employees\n| LIMIT 5\n```\n ", + "languageDocumentationPopover.documentationESQL.locate": "LOCATE", + "languageDocumentationPopover.documentationESQL.locate.markdown": "\n\n ### LOCATE\n 返回一个整数,指示关键字子字符串在另一字符串中的位置\n\n ```\n row a = \"hello\"\n | eval a_ll = locate(a, \"ll\")\n ```\n ", + "languageDocumentationPopover.documentationESQL.log": "LOG", + "languageDocumentationPopover.documentationESQL.log.markdown": "\n\n ### LOG\n 以某底数返回值的对数。输入可以为任何数字值,返回值始终为双精度值。\n\n 求零、负数的对数,以及底数为一时将返回 `null`,并显示警告。\n\n ```\n ROW base = 2.0, value = 8.0\n | EVAL s = LOG(base, value)\n ```\n ", + "languageDocumentationPopover.documentationESQL.log10": "LOG10", + "languageDocumentationPopover.documentationESQL.log10.markdown": "\n\n ### LOG10\n 以底数 10 返回值的对数。输入可以为任何数字值,返回值始终为双精度值。\n\n 求 0 和负数的对数时将返回 `null`,并显示警告。\n\n ```\n ROW d = 1000.0 \n | EVAL s = LOG10(d)\n ```\n ", + "languageDocumentationPopover.documentationESQL.ltrim": "LTRIM", + "languageDocumentationPopover.documentationESQL.ltrim.markdown": "\n\n ### LTRIM\n 从字符串中移除前导空格。\n\n ```\n ROW message = \" some text \", color = \" red \"\n | EVAL message = LTRIM(message)\n | EVAL color = LTRIM(color)\n | EVAL message = CONCAT(\"'\", message, \"'\")\n | EVAL color = CONCAT(\"'\", color, \"'\")\n ```\n ", + "languageDocumentationPopover.documentationESQL.markdown": "## ES|QL\n\nES|QL(Elasticsearch 查询语言)查询包含一系列命令,它们用管道字符分隔:`|`。每个查询以**源命令**开头,它会生成一个表,其中通常包含来自 Elasticsearch 的数据。\n\n源命令可后接一个或多个**处理命令**。处理命令可通过添加、移除以及更改行和列来更改前一个命令的输出表。\n\n```\nsource-command\n| processing-command1\n| processing-command2\n```\n\n查询的结果为由最后的处理命令生成的表。 \n ", + "languageDocumentationPopover.documentationESQL.mv_append": "MV_APPEND", + "languageDocumentationPopover.documentationESQL.mv_append.markdown": "\n\n ### MV_APPEND\n 串联两个多值字段的值。\n\n ", + "languageDocumentationPopover.documentationESQL.mv_avg": "MV_AVG", + "languageDocumentationPopover.documentationESQL.mv_avg.markdown": "\n\n ### MV_AVG\n 将多值字段转换为包含所有值的平均值的单值字段。\n\n ```\n ROW a=[3, 5, 1, 6]\n | EVAL avg_a = MV_AVG(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_concat": "MV_CONCAT", + "languageDocumentationPopover.documentationESQL.mv_concat.markdown": "\n\n ### MV_CONCAT\n 将多值字符串表达式转换为单值列,其中包含由分隔符分隔的所有值的串联形式。\n\n ```\n ROW a=[\"foo\", \"zoo\", \"bar\"]\n | EVAL j = MV_CONCAT(a, \", \")\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_count": "MV_COUNT", + "languageDocumentationPopover.documentationESQL.mv_count.markdown": "\n\n ### MV_COUNT\n 将多值表达式转换为包含值计数的单值列。\n\n ```\n ROW a=[\"foo\", \"zoo\", \"bar\"]\n | EVAL count_a = MV_COUNT(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_dedupe": "MV_DEDUPE", + "languageDocumentationPopover.documentationESQL.mv_dedupe.markdown": "\n\n ### MV_DEDUPE\n 移除多值字段中的重复值。\n\n ```\n ROW a=[\"foo\", \"foo\", \"bar\", \"foo\"]\n | EVAL dedupe_a = MV_DEDUPE(a)\n ```\n 注意:`MV_DEDUPE` 可能但不会始终对列中的值进行排序。\n ", + "languageDocumentationPopover.documentationESQL.mv_first": "MV_FIRST", + "languageDocumentationPopover.documentationESQL.mv_first.markdown": "\n\n ### MV_FIRST\n 将多值表达式转换为包含第一个值的\n 单值列。这在从按已知顺序发出多值列的\n 函数(如 `SPLIT`)中读取数据时尤其有用。\n\n 无法保证从底层存储\n 读取多值字段的顺序。它 *通常* 为升序,但不应\n 依赖于此。如果需要最小值,请使用 `MV_MIN` 而不是\n `MV_FIRST`。`MV_MIN` 针对排序值进行了优化,因此\n 对 `MV_FIRST` 没有性能优势。\n\n ```\n ROW a=\"foo;bar;baz\"\n | EVAL first_a = MV_FIRST(SPLIT(a, \";\"))\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_last": "MV_LAST", + "languageDocumentationPopover.documentationESQL.mv_last.markdown": "\n\n ### MV_LAST\n 将多值表达式转换为包含最后一个值的单值\n 列。这在从按已知顺序发出多值列的函数\n (如 `SPLIT`)中读取数据时尤其有用。\n\n 无法保证从底层存储\n 读取多值字段的顺序。它 *通常* 为升序,但不应\n 依赖于此。如果需要最大值,请使用 `MV_MAX` 而不是\n `MV_LAST`。`MV_MAX` 针对排序值进行了优化,因此\n 对 `MV_LAST` 没有性能优势。\n\n ```\n ROW a=\"foo;bar;baz\"\n | EVAL last_a = MV_LAST(SPLIT(a, \";\"))\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_max": "MV_MAX", + "languageDocumentationPopover.documentationESQL.mv_max.markdown": "\n\n ### MV_MAX\n 将多值表达式转换为包含最大值的单值列。\n\n ```\n ROW a=[3, 5, 1]\n | EVAL max_a = MV_MAX(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_median": "MV_MEDIAN", + "languageDocumentationPopover.documentationESQL.mv_median.markdown": "\n\n ### MV_MEDIAN\n 将多值字段转换为包含中位数值的单值字段。\n\n ```\n ROW a=[3, 5, 1]\n | EVAL median_a = MV_MEDIAN(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_min": "MV_MIN", + "languageDocumentationPopover.documentationESQL.mv_min.markdown": "\n\n ### MV_MIN\n 将多值表达式转换为包含最小值的单值列。\n\n ```\n ROW a=[2, 1]\n | EVAL min_a = MV_MIN(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_slice": "MV_SLICE", + "languageDocumentationPopover.documentationESQL.mv_slice.markdown": "\n\n ### MV_SLICE\n 使用起始和结束索引值返回多值字段的子集。\n\n ```\n row a = [1, 2, 2, 3]\n | eval a1 = mv_slice(a, 1), a2 = mv_slice(a, 2, 3)\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_sort": "MV_SORT", + "languageDocumentationPopover.documentationESQL.mv_sort.markdown": "\n\n ### MV_SORT\n 按字典顺序对多值字段排序。\n\n ```\n ROW a = [4, 2, -3, 2]\n | EVAL sa = mv_sort(a), sd = mv_sort(a, \"DESC\")\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_sum": "MV_SUM", + "languageDocumentationPopover.documentationESQL.mv_sum.markdown": "\n\n ### MV_SUM\n 将多值字段转换为包含所有值的总和的单值字段。\n\n ```\n ROW a=[3, 5, 6]\n | EVAL sum_a = MV_SUM(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.mv_zip": "MV_ZIP", + "languageDocumentationPopover.documentationESQL.mv_zip.markdown": "\n\n ### MV_ZIP\n 组合两个使用分隔符联接在一起的多值字段中的值。\n\n ```\n ROW a = [\"x\", \"y\", \"z\"], b = [\"1\", \"2\"]\n | EVAL c = mv_zip(a, b, \"-\")\n | KEEP a, b, c\n ```\n ", + "languageDocumentationPopover.documentationESQL.mvExpand": "MV_EXPAND", + "languageDocumentationPopover.documentationESQL.mvExpand.markdown": "### MV_EXPAND\n`MV_EXPAND` 处理命令将多值字段扩展成每个值一行,从而复制其他字段: \n```\nROW a=[1,2,3], b=\"b\", j=[\"a\",\"b\"]\n| MV_EXPAND a\n```\n ", + "languageDocumentationPopover.documentationESQL.now": "NOW", + "languageDocumentationPopover.documentationESQL.now.markdown": "\n\n ### NOW\n 返回当前日期和时间。\n\n ```\n ROW current_date = NOW()\n ```\n ", + "languageDocumentationPopover.documentationESQL.pi": "PI", + "languageDocumentationPopover.documentationESQL.pi.markdown": "\n\n ### PI\n 返回 Pi,即圆的周长与其直径的比率。\n\n ```\n ROW PI()\n ```\n ", + "languageDocumentationPopover.documentationESQL.pow": "POW", + "languageDocumentationPopover.documentationESQL.pow.markdown": "\n\n ### POW\n 返回提升为 `exponent` 幂的 `base` 的值。\n\n ```\n ROW base = 2.0, exponent = 2\n | EVAL result = POW(base, exponent)\n ```\n 注意:此处仍可能使双精度结果溢出;在该情况下,将返回 null。\n ", + "languageDocumentationPopover.documentationESQL.predicates": "Null 值", + "languageDocumentationPopover.documentationESQL.predicates.markdown": "### NULL 值\n对于 NULL 比较,请使用 `IS NULL` 和 `IS NOT NULL` 谓词:\n\n```\nFROM employees\n| WHERE birth_date IS NULL\n| KEEP first_name, last_name\n| SORT first_name\n| LIMIT 3\n```\n\n```\nFROM employees\n| WHERE is_rehired IS NOT NULL\n| STATS count(emp_no)\n```\n ", + "languageDocumentationPopover.documentationESQL.rename": "RENAME", + "languageDocumentationPopover.documentationESQL.rename.markdown": "### RENAME\n请使用 `RENAME` 通过以下语法对列重命名:\n\n```\nRENAME AS \n```\n\n例如:\n\n```\nFROM employees\n| KEEP first_name, last_name, still_hired\n| RENAME still_hired AS employed\n```\n\n如果使用新名称的列已存在,将用新列替换该列。\n\n可以使用单个 `RENAME` 命令对多个列重命名:\n\n```\nFROM employees\n| KEEP first_name, last_name\n| RENAME first_name AS fn, last_name AS ln\n```\n ", + "languageDocumentationPopover.documentationESQL.repeat": "REPEAT", + "languageDocumentationPopover.documentationESQL.repeat.markdown": "\n\n ### REPEAT\n 返回通过串联 `string` 自身与指定次数 `number` 构造而成的字符串。\n\n ```\n ROW a = \"Hello!\"\n | EVAL triple_a = REPEAT(a, 3);\n ```\n ", + "languageDocumentationPopover.documentationESQL.replace": "REPLACE", + "languageDocumentationPopover.documentationESQL.replace.markdown": "\n\n ### REPLACE\n 此函数将字符串 `str` 中正则表达式 `regex` 的任何匹配项\n 替换为替代字符串 `newStr`。\n\n ```\n ROW str = \"Hello World\"\n | EVAL str = REPLACE(str, \"World\", \"Universe\")\n | KEEP str\n ```\n ", + "languageDocumentationPopover.documentationESQL.right": "RIGHT", + "languageDocumentationPopover.documentationESQL.right.markdown": "\n\n ### RIGHT\n 返回从“字符串”中提取“长度”字符的子字符串,从右侧开始。\n\n ```\n FROM employees\n | KEEP last_name\n | EVAL right = RIGHT(last_name, 3)\n | SORT last_name ASC\n | LIMIT 5\n ```\n ", + "languageDocumentationPopover.documentationESQL.round": "ROUND", + "languageDocumentationPopover.documentationESQL.round.markdown": "\n\n ### ROUND\n 将数字舍入到指定小数位数。\n 默认值为 0,即返回最近的整数。如果\n 精确度为负数,则将数字舍入到\n 小数点左侧的位数。\n\n ```\n FROM employees\n | KEEP first_name, last_name, height\n | EVAL height_ft = ROUND(height * 3.281, 1)\n ```\n ", + "languageDocumentationPopover.documentationESQL.row": "ROW", + "languageDocumentationPopover.documentationESQL.row.markdown": "### ROW\n`ROW` 源命令会生成一个行,其中包含一个或多个含有您指定的值的列。这可以用于测试。\n \n```\nROW a = 1, b = \"two\", c = null\n```\n\n请使用方括号创建多值列:\n\n```\nROW a = [2, 1]\n```\n\nROW 支持使用函数:\n\n```\nROW a = ROUND(1.23, 0)\n```\n ", + "languageDocumentationPopover.documentationESQL.rtrim": "RTRIM", + "languageDocumentationPopover.documentationESQL.rtrim.markdown": "\n\n ### RTRIM\n 从字符串中移除尾随空格。\n\n ```\n ROW message = \" some text \", color = \" red \"\n | EVAL message = RTRIM(message)\n | EVAL color = RTRIM(color)\n | EVAL message = CONCAT(\"'\", message, \"'\")\n | EVAL color = CONCAT(\"'\", color, \"'\")\n ```\n ", + "languageDocumentationPopover.documentationESQL.show": "SHOW", + "languageDocumentationPopover.documentationESQL.show.markdown": "### SHOW\n`SHOW ` 源命令返回有关部署及其功能的信息:\n\n* 使用 `SHOW INFO` 可返回部署的版本、构建日期和哈希。\n* 使用 `SHOW FUNCTIONS` 可返回所有受支持函数的列表和每个函数的概要。\n ", + "languageDocumentationPopover.documentationESQL.signum": "SIGNUM", + "languageDocumentationPopover.documentationESQL.signum.markdown": "\n\n ### SIGNUM\n 返回给定数字的符号。\n 它对负数返回 `-1`,对 `0` 返回 `0`,对正数返回 `1`。\n\n ```\n ROW d = 100.0\n | EVAL s = SIGNUM(d)\n ```\n ", + "languageDocumentationPopover.documentationESQL.sin": "SIN", + "languageDocumentationPopover.documentationESQL.sin.markdown": "\n\n ### SIN\n 返回角度的正弦三角函数。\n\n ```\n ROW a=1.8 \n | EVAL sin=SIN(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.sinh": "SINH", + "languageDocumentationPopover.documentationESQL.sinh.markdown": "\n\n ### SINH\n 返回角度的双曲正弦。\n\n ```\n ROW a=1.8 \n | EVAL sinh=SINH(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.sort": "SORT", + "languageDocumentationPopover.documentationESQL.sort.markdown": "### SORT\n使用 `SORT` 命令可对一个或多个字段上的行排序:\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height\n```\n\n默认排序顺序为升序。请使用 `ASC` 或 `DESC` 设置显式排序顺序:\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height DESC\n```\n\n如果两个行具有相同的排序键,则保留原始顺序。您可以提供其他排序表达式充当连接断路器:\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| SORT height DESC, first_name ASC\n```\n\n#### `null` 值\n默认情况下,会将 `null` 值视为大于任何其他值。使用升序排序顺序时,会最后对 `null` 值排序,而使用降序排序顺序时,会首先对 `null` 值排序。您可以通过提供 `NULLS FIRST` 或 `NULLS LAST` 来更改该排序:\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| SORT first_name ASC NULLS FIRST\n```\n ", + "languageDocumentationPopover.documentationESQL.split": "SPLIT", + "languageDocumentationPopover.documentationESQL.split.markdown": "\n\n ### SPLIT\n 将单值字符串拆分成多个字符串。\n\n ```\n ROW words=\"foo;bar;baz;qux;quux;corge\"\n | EVAL word = SPLIT(words, \";\")\n ```\n ", + "languageDocumentationPopover.documentationESQL.sqrt": "SQRT", + "languageDocumentationPopover.documentationESQL.sqrt.markdown": "\n\n ### SQRT\n 返回数字的平方根。输入可以为任何数字值,返回值始终为双精度值。\n 负数和无穷大的平方根为 null。\n\n ```\n ROW d = 100.0\n | EVAL s = SQRT(d)\n ```\n ", + "languageDocumentationPopover.documentationESQL.st_contains": "ST_CONTAINS", + "languageDocumentationPopover.documentationESQL.st_contains.markdown": "\n\n ### ST_CONTAINS\n 返回第一个几何形状是否包含第二个几何形状。\n 这是 `ST_WITHIN` 函数的反向函数。\n\n ```\n FROM airport_city_boundaries\n | WHERE ST_CONTAINS(city_boundary, TO_GEOSHAPE(\"POLYGON((109.35 18.3, 109.45 18.3, 109.45 18.4, 109.35 18.4, 109.35 18.3))\"))\n | KEEP abbrev, airport, region, city, city_location\n ```\n ", + "languageDocumentationPopover.documentationESQL.st_disjoint": "ST_DISJOINT", + "languageDocumentationPopover.documentationESQL.st_disjoint.markdown": "\n\n ### ST_DISJOINT\n 返回两个几何图形或几何图形列是否不相交。\n 这是 `ST_INTERSECTS` 函数的反向函数。\n 从数学上讲:ST_Disjoint(A, B) ⇔ A ⋂ B = ∅\n\n ```\n FROM airport_city_boundaries\n | WHERE ST_DISJOINT(city_boundary, TO_GEOSHAPE(\"POLYGON((-10 -60, 120 -60, 120 60, -10 60, -10 -60))\"))\n | KEEP abbrev, airport, region, city, city_location\n ```\n ", + "languageDocumentationPopover.documentationESQL.st_distance": "ST_DISTANCE", + "languageDocumentationPopover.documentationESQL.st_distance.markdown": "\n\n ### ST_DISTANCE\n 计算两点之间的距离。\n 对于笛卡尔几何形状,这是以相同单位作为原始坐标时的毕达哥拉斯距离。\n 对于地理几何形状而言,这是沿着地球大圆的圆周距离(以米为单位)。\n\n ```\n FROM airports\n | WHERE abbrev == \"CPH\"\n | EVAL distance = ST_DISTANCE(location, city_location)\n | KEEP abbrev, name, location, city_location, distance\n ```\n ", + "languageDocumentationPopover.documentationESQL.st_intersects": "ST_INTERSECTS", + "languageDocumentationPopover.documentationESQL.st_intersects.markdown": "\n\n ### ST_INTERSECTS\n 如果两个几何形状相交,则返回 true。\n 如果它们有任何共同点,包括其内点\n (沿线的点或多边形内的点),则表示它们相交。\n 这是 `ST_DISJOINT` 函数的反向函数。\n 从数学上讲:ST_Intersects(A, B) ⇔ A ⋂ B ≠ ∅\n\n ```\n FROM airports\n | WHERE ST_INTERSECTS(location, TO_GEOSHAPE(\"POLYGON((42 14, 43 14, 43 15, 42 15, 42 14))\"))\n ```\n ", + "languageDocumentationPopover.documentationESQL.st_within": "ST_WITHIN", + "languageDocumentationPopover.documentationESQL.st_within.markdown": "\n\n ### ST_WITHIN\n 返回第一个几何形状是否在第二个几何形状内。\n 这是 `ST_CONTAINS` 函数的反向函数。\n\n ```\n FROM airport_city_boundaries\n | WHERE ST_WITHIN(city_boundary, TO_GEOSHAPE(\"POLYGON((109.1 18.15, 109.6 18.15, 109.6 18.65, 109.1 18.65, 109.1 18.15))\"))\n | KEEP abbrev, airport, region, city, city_location\n ```\n ", + "languageDocumentationPopover.documentationESQL.st_x": "ST_X", + "languageDocumentationPopover.documentationESQL.st_x.markdown": "\n\n ### ST_X\n 从提供的点中提取 `x` 坐标。\n 如果点的类型为 `geo_point`,则这等同于提取 `longitude` 值。\n\n ```\n ROW point = TO_GEOPOINT(\"POINT(42.97109629958868 14.7552534006536)\")\n | EVAL x = ST_X(point), y = ST_Y(point)\n ```\n ", + "languageDocumentationPopover.documentationESQL.st_y": "ST_Y", + "languageDocumentationPopover.documentationESQL.st_y.markdown": "\n\n ### ST_Y\n 从提供的点中提取 `y` 坐标。\n 如果点的类型为 `geo_point`,则这等同于提取 `latitude` 值。\n\n ```\n ROW point = TO_GEOPOINT(\"POINT(42.97109629958868 14.7552534006536)\")\n | EVAL x = ST_X(point), y = ST_Y(point)\n ```\n ", + "languageDocumentationPopover.documentationESQL.starts_with": "STARTS_WITH", + "languageDocumentationPopover.documentationESQL.starts_with.markdown": "\n\n ### STARTS_WITH\n 返回指示关键字字符串是否以另一个字符串开头的布尔值。\n\n ```\n FROM employees\n | KEEP last_name\n | EVAL ln_S = STARTS_WITH(last_name, \"B\")\n ```\n ", + "languageDocumentationPopover.documentationESQL.statsby": "STATS ...BY", + "languageDocumentationPopover.documentationESQL.statsby.markdown": "### STATS ...BY\n使用 `STATS ...BY` 可根据公共值对行分组,并计算已分组行中的一个或多个聚合值。\n\n**示例**:\n\n```\nFROM employees\n| STATS count = COUNT(emp_no) BY languages\n| SORT languages\n```\n\n如果省略 `BY`,输出表实际将包含一行,其中为应用于整个数据集的聚合:\n\n```\nFROM employees\n| STATS avg_lang = AVG(languages)\n```\n\n可以计算多个值:\n\n```\nFROM employees\n| STATS avg_lang = AVG(languages), max_lang = MAX(languages)\n```\n\n也可以按多个值分组(仅长整型和关键字家族字段支持):\n\n```\nFROM employees\n| EVAL hired = DATE_FORMAT(hire_date, \"YYYY\")\n| STATS avg_salary = AVG(salary) BY hired, languages.long\n| EVAL avg_salary = ROUND(avg_salary)\n| SORT hired, languages.long\n```\n\n请参阅**聚合函数**获取可与 `STATS ...BY` 搭配使用的函数列表。\n\n聚合函数和分组表达式均接受其他函数。这在对多值列使用 `STATS...BY` 时有用。例如,要计算平均工资变动,可以首先使用 `MV_AVG` 对每名员工的多个值求平均值,然后将结果用于 `AVG` 函数:\n\n```\nFROM employees\n| STATS avg_salary_change = AVG(MV_AVG(salary_change))\n```\n\n按表达式分组的示例为根据员工姓氏的第一个字母对其进行分组:\n\n```\nFROM employees\n| STATS my_count = COUNT() BY LEFT(last_name, 1)\n| SORT `LEFT(last_name, 1)`\n```\n\n指定输出列名称为可选操作。如果未指定,新列名称等于该表达式。以下查询将返回名为 `AVG(salary)` 的列:\n\n```\nFROM employees\n| STATS AVG(salary)\n```\n\n由于此名称包含特殊字符,在后续命令中使用该名称时,需要用反撇号 (`) 引用它:\n\n```\nFROM employees\n| STATS AVG(salary)\n| EVAL avg_salary_rounded = ROUND(`AVG(salary)`)\n```\n\n**注意**:不包含任何组的 `STATS` 比添加组更快。\n\n**注意**:当前,根据单一表达式进行分组比根据许多表达式进行分组更为优化。\n ", + "languageDocumentationPopover.documentationESQL.stringOperators": "LIKE 和 RLIKE", + "languageDocumentationPopover.documentationESQL.stringOperators.markdown": "### LIKE 和 RLIKE\n使用通配符或正则表达式比较字符串时,请使用 `LIKE` 或 `RLIKE`:\n\n使用 `LIKE` 时,可使用通配符来匹配字符串。支持以下通配符字符:\n\n* `*` 匹配零个或更多字符。\n* `?` 匹配一个字符。\n\n```\nFROM employees\n| WHERE first_name LIKE \"?b*\"\n| KEEP first_name, last_name\n```\n\n使用 `RLIKE` 时,可使用正则表达式来匹配字符串:\n\n```\nFROM employees\n| WHERE first_name RLIKE \".leja.*\"\n| KEEP first_name, last_name\n```\n ", + "languageDocumentationPopover.documentationESQL.substring": "SUBSTRING", + "languageDocumentationPopover.documentationESQL.substring.markdown": "\n\n ### SUBSTRING\n 返回字符串的子字符串,用起始位置和可选长度指定\n\n ```\n FROM employees\n | KEEP last_name\n | EVAL ln_sub = SUBSTRING(last_name, 1, 3)\n ```\n ", + "languageDocumentationPopover.documentationESQL.tan": "TAN", + "languageDocumentationPopover.documentationESQL.tan.markdown": "\n\n ### TAN\n 返回角度的正切三角函数。\n\n ```\n ROW a=1.8 \n | EVAL tan=TAN(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.tanh": "TANH", + "languageDocumentationPopover.documentationESQL.tanh.markdown": "\n\n ### TANH\n 返回角度的双曲正切函数。\n\n ```\n ROW a=1.8 \n | EVAL tanh=TANH(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.tau": "TAU", + "languageDocumentationPopover.documentationESQL.tau.markdown": "\n\n ### TAU\n 返回圆的圆周长与其半径的比率。\n\n ```\n ROW TAU()\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_base64": "TO_BASE64", + "languageDocumentationPopover.documentationESQL.to_base64.markdown": "\n\n ### TO_BASE64\n 将字符串编码为 base64 字符串。\n\n ```\n row a = \"elastic\" \n | eval e = to_base64(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_boolean": "TO_BOOLEAN", + "languageDocumentationPopover.documentationESQL.to_boolean.markdown": "\n\n ### TO_BOOLEAN\n 将输入值转换为布尔值。\n 字符串值 *true* 将不区分大小写并被转换为布尔值 *true*。\n 对于任何其他值,包括空字符串,此函数将返回 *false*。\n 数字值 *0* 将转换为 *false*,任何其他值将转换为 *true*。\n\n ```\n ROW str = [\"true\", \"TRuE\", \"false\", \"\", \"yes\", \"1\"]\n | EVAL bool = TO_BOOLEAN(str)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_cartesianpoint": "TO_CARTESIANPOINT", + "languageDocumentationPopover.documentationESQL.to_cartesianpoint.markdown": "\n\n ### TO_CARTESIANPOINT\n 将输入值转换为 `cartesian_point` 值。\n 字符串只有符合 WKT 点格式时,才能成功转换。\n\n ```\n ROW wkt = [\"POINT(4297.11 -1475.53)\", \"POINT(7580.93 2272.77)\"]\n | MV_EXPAND wkt\n | EVAL pt = TO_CARTESIANPOINT(wkt)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_cartesianshape": "TO_CARTESIANSHAPE", + "languageDocumentationPopover.documentationESQL.to_cartesianshape.markdown": "\n\n ### TO_CARTESIANSHAPE\n 将输入值转换为 `cartesian_shape` 值。\n 字符串只有符合 WKT 格式时,才能成功转换。\n\n ```\n ROW wkt = [\"POINT(4297.11 -1475.53)\", \"POLYGON ((3339584.72 1118889.97, 4452779.63 4865942.27, 2226389.81 4865942.27, 1113194.90 2273030.92, 3339584.72 1118889.97))\"]\n | MV_EXPAND wkt\n | EVAL geom = TO_CARTESIANSHAPE(wkt)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_datetime": "TO_DATETIME", + "languageDocumentationPopover.documentationESQL.to_datetime.markdown": "\n\n ### TO_DATETIME\n 将输入值转换为日期值。\n 仅当字符串采用 `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` 格式时,才可进行成功转换。\n 要转换其他格式的日期,请使用 `DATE_PARSE`。\n\n ```\n ROW string = [\"1953-09-02T00:00:00.000Z\", \"1964-06-02T00:00:00.000Z\", \"1964-06-02 00:00:00\"]\n | EVAL datetime = TO_DATETIME(string)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_degrees": "TO_DEGREES", + "languageDocumentationPopover.documentationESQL.to_degrees.markdown": "\n\n ### TO_DEGREES\n 将弧度转换为度数。\n\n ```\n ROW rad = [1.57, 3.14, 4.71]\n | EVAL deg = TO_DEGREES(rad)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_double": "TO_DOUBLE", + "languageDocumentationPopover.documentationESQL.to_double.markdown": "\n\n ### TO_DOUBLE\n 将输入值转换为双精度值。如果输入参数为日期类型,\n 会将其值解析为自 Unix epoch 以来的毫秒数,\n 并转换为双精度值。布尔值 *true* 将转换为双精度值 *1.0*,*false* 转换为 *0.0*。\n\n ```\n ROW str1 = \"5.20128E11\", str2 = \"foo\"\n | EVAL dbl = TO_DOUBLE(\"520128000000\"), dbl1 = TO_DOUBLE(str1), dbl2 = TO_DOUBLE(str2)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_geopoint": "TO_GEOPOINT", + "languageDocumentationPopover.documentationESQL.to_geopoint.markdown": "\n\n ### TO_GEOPOINT\n 将输入值转换为 `geo_point` 值。\n 字符串只有符合 WKT 点格式时,才能成功转换。\n\n ```\n ROW wkt = \"POINT(42.97109630194 14.7552534413725)\"\n | EVAL pt = TO_GEOPOINT(wkt)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_geoshape": "TO_GEOSHAPE", + "languageDocumentationPopover.documentationESQL.to_geoshape.markdown": "\n\n ### TO_GEOSHAPE\n 将输入值转换为 `geo_shape` 值。\n 字符串只有符合 WKT 格式时,才能成功转换。\n\n ```\n ROW wkt = \"POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))\"\n | EVAL geom = TO_GEOSHAPE(wkt)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_integer": "TO_INTEGER", + "languageDocumentationPopover.documentationESQL.to_integer.markdown": "\n\n ### TO_INTEGER\n 将输入值转换为整数值。\n 如果输入参数为日期类型,会将其值解析为自 Unix epoch 以来\n 的毫秒数,并转换为整数。\n 布尔值 *true* 将转换为整数 *1*,*false* 转换为 *0*。\n\n ```\n ROW long = [5013792, 2147483647, 501379200000]\n | EVAL int = TO_INTEGER(long)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_ip": "TO_IP", + "languageDocumentationPopover.documentationESQL.to_ip.markdown": "\n\n ### TO_IP\n 将输入字符串转换为 IP 值。\n\n ```\n ROW str1 = \"1.1.1.1\", str2 = \"foo\"\n | EVAL ip1 = TO_IP(str1), ip2 = TO_IP(str2)\n | WHERE CIDR_MATCH(ip1, \"1.0.0.0/8\")\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_long": "TO_LONG", + "languageDocumentationPopover.documentationESQL.to_long.markdown": "\n\n ### TO_LONG\n 将输入值转换为长整型值。如果输入参数为日期类型,\n 会将其值解析为自 Unix epoch 以来的毫秒数,并转换为长整型值。\n 布尔值 *true* 将转换为长整型值 *1*,*false* 转换为 *0*。\n\n ```\n ROW str1 = \"2147483648\", str2 = \"2147483648.2\", str3 = \"foo\"\n | EVAL long1 = TO_LONG(str1), long2 = TO_LONG(str2), long3 = TO_LONG(str3)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_lower": "TO_LOWER", + "languageDocumentationPopover.documentationESQL.to_lower.markdown": "\n\n ### TO_LOWER\n 返回一个新字符串,表示已将输入字符串转为小写。\n\n ```\n ROW message = \"Some Text\"\n | EVAL message_lower = TO_LOWER(message)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_radians": "TO_RADIANS", + "languageDocumentationPopover.documentationESQL.to_radians.markdown": "\n\n ### TO_RADIANS\n 将度数转换为弧度。\n\n ```\n ROW deg = [90.0, 180.0, 270.0]\n | EVAL rad = TO_RADIANS(deg)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_string": "TO_STRING", + "languageDocumentationPopover.documentationESQL.to_string.markdown": "\n\n ### TO_STRING\n 将输入值转换为字符串。\n\n ```\n ROW a=10\n | EVAL j = TO_STRING(a)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_unsigned_long": "TO_UNSIGNED_LONG", + "languageDocumentationPopover.documentationESQL.to_unsigned_long.markdown": "\n\n ### TO_UNSIGNED_LONG\n 将输入值转换为无符号长整型值。如果输入参数为日期类型,\n 会将其值解析为自 Unix epoch 以来的毫秒数,并转换为无符号长整型值。\n 布尔值 *true* 将转换为无符号长整型值 *1*,*false* 转换为 *0*。\n\n ```\n ROW str1 = \"2147483648\", str2 = \"2147483648.2\", str3 = \"foo\"\n | EVAL long1 = TO_UNSIGNED_LONG(str1), long2 = TO_ULONG(str2), long3 = TO_UL(str3)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_upper": "TO_UPPER", + "languageDocumentationPopover.documentationESQL.to_upper.markdown": "\n\n ### TO_UPPER\n 返回一个新字符串,表示已将输入字符串转为大写。\n\n ```\n ROW message = \"Some Text\"\n | EVAL message_upper = TO_UPPER(message)\n ```\n ", + "languageDocumentationPopover.documentationESQL.to_version": "TO_VERSION", + "languageDocumentationPopover.documentationESQL.to_version.markdown": "\n\n ### TO_VERSION\n 将输入字符串转换为版本值。\n\n ```\n ROW v = TO_VERSION(\"1.2.3\")\n ```\n ", + "languageDocumentationPopover.documentationESQL.trim": "TRIM", + "languageDocumentationPopover.documentationESQL.trim.markdown": "\n\n ### TRIM\n 从字符串中移除前导和尾随空格。\n\n ```\n ROW message = \" some text \", color = \" red \"\n | EVAL message = TRIM(message)\n | EVAL color = TRIM(color)\n ```\n ", + "languageDocumentationPopover.documentationESQL.where": "WHERE", + "languageDocumentationPopover.documentationESQL.where.markdown": "### WHERE\n使用 `WHERE` 可生成一个表,其中包含输入表中所提供的条件评估为 `true` 的所有行:\n \n```\nFROM employees\n| KEEP first_name, last_name, still_hired\n| WHERE still_hired == true\n```\n\n#### 运算符\n\n请参阅**运算符**了解所支持的运算符的概览。\n\n#### 函数\n`WHERE` 支持各种用于计算值的函数。请参阅**函数**了解更多信息。\n ", "textBasedEditor.query.textBasedLanguagesEditor.EnableWordWrapLabel": "在管道符上添加换行符", "textBasedEditor.query.textBasedLanguagesEditor.errorCount": "{count} 个{count, plural, other {错误}}", "textBasedEditor.query.textBasedLanguagesEditor.errorsTitle": "错误", - "textBasedEditor.query.textBasedLanguagesEditor.esql": "ES|QL", "textBasedEditor.query.textBasedLanguagesEditor.expandLabel": "展开", "textBasedEditor.query.textBasedLanguagesEditor.feedback": "反馈", - "textBasedEditor.query.textBasedLanguagesEditor.functions": "函数", - "textBasedEditor.query.textBasedLanguagesEditor.functionsDocumentationESQLDescription": "ROW、EVAL 和 WHERE 支持的函数。", - "textBasedEditor.query.textBasedLanguagesEditor.groupingFunctions": "分组函数", - "textBasedEditor.query.textBasedLanguagesEditor.groupingFunctionsDocumentationESQLDescription": "这些分组函数可以与 `STATS...BY` 搭配使用:", + "languageDocumentationPopover.documentationESQL.functions": "函数", + "languageDocumentationPopover.documentationESQL.functionsDocumentationESQLDescription": "ROW、EVAL 和 WHERE 支持的函数。", + "languageDocumentationPopover.documentationESQL.groupingFunctions": "分组函数", + "languageDocumentationPopover.documentationESQL.groupingFunctionsDocumentationESQLDescription": "这些分组函数可以与 `STATS...BY` 搭配使用:", "textBasedEditor.query.textBasedLanguagesEditor.hideQueriesLabel": "隐藏最近查询", "textBasedEditor.query.textBasedLanguagesEditor.lineCount": "{count} {count, plural, other {行}}", "textBasedEditor.query.textBasedLanguagesEditor.lineNumber": "第 {lineNumber} 行", - "textBasedEditor.query.textBasedLanguagesEditor.operators": "运算符", - "textBasedEditor.query.textBasedLanguagesEditor.operatorsDocumentationESQLDescription": "ES|QL 支持以下运算符:", - "textBasedEditor.query.textBasedLanguagesEditor.processingCommands": "处理命令", - "textBasedEditor.query.textBasedLanguagesEditor.processingCommandsDescription": "处理命令会通过添加、移除或更改行和列来更改输入表。ES|QL 支持以下处理命令。", + "languageDocumentationPopover.documentationESQL.operators": "运算符", + "languageDocumentationPopover.documentationESQL.operatorsDocumentationESQLDescription": "ES|QL 支持以下运算符:", + "languageDocumentationPopover.documentationESQL.processingCommands": "处理命令", + "languageDocumentationPopover.documentationESQL.processingCommandsDescription": "处理命令会通过添加、移除或更改行和列来更改输入表。ES|QL 支持以下处理命令。", "textBasedEditor.query.textBasedLanguagesEditor.querieshistory.error": "查询失败", "textBasedEditor.query.textBasedLanguagesEditor.querieshistory.success": "已成功运行查询", "textBasedEditor.query.textBasedLanguagesEditor.querieshistoryCopy": "复制查询到剪贴板", @@ -7347,7 +7345,7 @@ "textBasedEditor.query.textBasedLanguagesEditor.recentQueriesColumnLabel": "最近查询", "textBasedEditor.query.textBasedLanguagesEditor.runQuery": "运行查询", "textBasedEditor.query.textBasedLanguagesEditor.showQueriesLabel": "显示最近查询", - "textBasedEditor.query.textBasedLanguagesEditor.sourceCommands": "源命令", + "languageDocumentationPopover.documentationESQL.sourceCommands": "源命令", "textBasedEditor.query.textBasedLanguagesEditor.submitFeedback": "提交反馈", "textBasedEditor.query.textBasedLanguagesEditor.timeRanColumnLabel": "运行时间", "textBasedEditor.query.textBasedLanguagesEditor.timestampNotDetected": "未找到 @timestamp", From 446d59342da39a9e66c306f72991b2d38c4dd826 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 20 Sep 2024 03:35:12 +1000 Subject: [PATCH 19/19] [8.x] [Global Search, Saved Objects Management] Use new parse option to specify recognized fields (#190464) (#193448) # Backport This will backport the following commits from `main` to `8.x`: - [[Global Search, Saved Objects Management] Use new parse option to specify recognized fields (#190464)](https://github.com/elastic/kibana/pull/190464) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Tim Sullivan --- .../public/finder/saved_object_finder.tsx | 3 +++ .../__snapshots__/table.test.tsx.snap | 12 ++++++++++++ .../objects_table/components/table.tsx | 7 ++++++- .../search_syntax/parse_search_params.test.ts | 15 +++------------ .../search_syntax/parse_search_params.ts | 19 +++++-------------- .../public/search_syntax/types.ts | 4 ---- 6 files changed, 29 insertions(+), 31 deletions(-) diff --git a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx index b280e626467d7..86940d52a81b3 100644 --- a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx +++ b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx @@ -349,6 +349,9 @@ export class SavedObjectFinderUi extends React.Component< box: { incremental: true, 'data-test-subj': 'savedObjectFinderSearchInput', + schema: { + recognizedFields: ['type', 'tag'], + }, }, filters: this.props.showFilter ? [ diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap index 7e88de9674cc4..bc0eda7e6d5a5 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap @@ -6,6 +6,12 @@ exports[`Table prevents hidden saved objects from being deleted 1`] = ` box={ Object { "data-test-subj": "savedObjectSearchBar", + "schema": Object { + "recognizedFields": Array [ + "type", + "tag", + ], + }, } } filters={ @@ -234,6 +240,12 @@ exports[`Table should render normally 1`] = ` box={ Object { "data-test-subj": "savedObjectSearchBar", + "schema": Object { + "recognizedFields": Array [ + "type", + "tag", + ], + }, } } filters={ diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx index dbd9b2a550605..a32a1e9e958e1 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx @@ -392,7 +392,12 @@ export class Table extends PureComponent { {activeActionContents} { const searchParams = parseSearchParams('tag:((()^invalid'); expect(searchParams).toEqual({ term: 'tag:((()^invalid', - filters: { - unknowns: {}, - }, + filters: {}, }); }); @@ -33,7 +31,6 @@ describe('parseSearchParams', () => { expect(searchParams.filters).toEqual({ tags: undefined, types: undefined, - unknowns: {}, }); }); @@ -44,20 +41,16 @@ describe('parseSearchParams', () => { filters: { tags: ['foo', 'dolly'], types: ['bar'], - unknowns: {}, }, }); }); - it('handles unknowns field clauses', () => { + it('considers unknown field clauses to be part of the raw search term', () => { const searchParams = parseSearchParams('tag:foo unknown:bar hello'); expect(searchParams).toEqual({ - term: 'hello', + term: 'unknown:bar hello', filters: { tags: ['foo'], - unknowns: { - unknown: ['bar'], - }, }, }); }); @@ -69,7 +62,6 @@ describe('parseSearchParams', () => { filters: { tags: ['foo', 'bar'], types: ['dash', 'board'], - unknowns: {}, }, }); }); @@ -81,7 +73,6 @@ describe('parseSearchParams', () => { filters: { tags: ['42', 'true'], types: ['69', 'false'], - unknowns: {}, }, }); }); diff --git a/x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.ts b/x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.ts index 989accdf2ea11..1df6c1123a328 100644 --- a/x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.ts +++ b/x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.ts @@ -17,32 +17,24 @@ const aliasMap = { }; export const parseSearchParams = (term: string): ParsedSearchParams => { + const recognizedFields = knownFilters.concat(...Object.values(aliasMap)); let query: Query; try { - query = Query.parse(term); + query = Query.parse(term, { + schema: { recognizedFields }, + }); } catch (e) { // if the query fails to parse, we just perform the search against the raw search term. return { term, - filters: { - unknowns: {}, - }, + filters: {}, }; } const searchTerm = getSearchTerm(query); const filterValues = applyAliases(getFieldValueMap(query), aliasMap); - const unknownFilters = [...filterValues.entries()] - .filter(([key]) => !knownFilters.includes(key)) - .reduce((unknowns, [key, value]) => { - return { - ...unknowns, - [key]: value, - }; - }, {} as Record); - const tags = filterValues.get('tag'); const types = filterValues.get('type'); @@ -51,7 +43,6 @@ export const parseSearchParams = (term: string): ParsedSearchParams => { filters: { tags: tags ? valuesToString(tags) : undefined, types: types ? valuesToString(types) : undefined, - unknowns: unknownFilters, }, }; }; diff --git a/x-pack/plugins/global_search_bar/public/search_syntax/types.ts b/x-pack/plugins/global_search_bar/public/search_syntax/types.ts index dc931a469fd7c..7b1ce5b762c87 100644 --- a/x-pack/plugins/global_search_bar/public/search_syntax/types.ts +++ b/x-pack/plugins/global_search_bar/public/search_syntax/types.ts @@ -27,9 +27,5 @@ export interface ParsedSearchParams { * Aggregation of `type` and `types` field clauses */ types?: FilterValues; - /** - * All unknown field clauses - */ - unknowns: Record; }; }