From 2507c47992f03309c5e4a8619dcafaa367abdcd5 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Wed, 23 Jun 2021 12:30:55 +0200 Subject: [PATCH 01/43] Ingest pipeline locator (#102878) (#103053) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 implement ingest pipeline locator * feat: 🎸 improve ingest pipeline locator * feat: 🎸 register ingest pipeline locator * refactor: 💡 use locator in expand_row component * chore: 🤖 remove ingest pipelines URL generator * fix: 🐛 correct TypeScript errors Co-authored-by: Vadim Kibana Co-authored-by: Vadim Kibana --- src/plugins/share/public/index.ts | 3 +- .../plugins/ingest_pipelines/public/index.ts | 7 -- .../ingest_pipelines/public/locator.test.ts | 100 ++++++++++++++++ .../ingest_pipelines/public/locator.ts | 102 +++++++++++++++++ .../plugins/ingest_pipelines/public/plugin.ts | 8 +- .../public/url_generator.test.ts | 108 ------------------ .../ingest_pipelines/public/url_generator.ts | 99 ---------------- .../models_management/expanded_row.tsx | 24 ++-- 8 files changed, 220 insertions(+), 231 deletions(-) create mode 100644 x-pack/plugins/ingest_pipelines/public/locator.test.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/locator.ts delete mode 100644 x-pack/plugins/ingest_pipelines/public/url_generator.test.ts delete mode 100644 x-pack/plugins/ingest_pipelines/public/url_generator.ts diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index 8f5356f6a2201..5ee3156534c5e 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -7,7 +7,8 @@ */ export { CSV_QUOTE_VALUES_SETTING, CSV_SEPARATOR_SETTING } from '../common/constants'; -export { LocatorDefinition } from '../common/url_service'; + +export { LocatorDefinition, LocatorPublic, KibanaLocation } from '../common/url_service'; export { UrlGeneratorStateMapping } from './url_generators/url_generator_definition'; diff --git a/x-pack/plugins/ingest_pipelines/public/index.ts b/x-pack/plugins/ingest_pipelines/public/index.ts index 8948a3e8d56be..d120f60ef8a2d 100644 --- a/x-pack/plugins/ingest_pipelines/public/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/index.ts @@ -10,10 +10,3 @@ import { IngestPipelinesPlugin } from './plugin'; export function plugin() { return new IngestPipelinesPlugin(); } - -export { - INGEST_PIPELINES_APP_ULR_GENERATOR, - IngestPipelinesUrlGenerator, - IngestPipelinesUrlGeneratorState, - INGEST_PIPELINES_PAGES, -} from './url_generator'; diff --git a/x-pack/plugins/ingest_pipelines/public/locator.test.ts b/x-pack/plugins/ingest_pipelines/public/locator.test.ts new file mode 100644 index 0000000000000..0b1246b2bed59 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/locator.test.ts @@ -0,0 +1,100 @@ +/* + * Copyright 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 { ManagementAppLocatorDefinition } from 'src/plugins/management/common/locator'; +import { IngestPipelinesLocatorDefinition, INGEST_PIPELINES_PAGES } from './locator'; + +describe('Ingest pipeline locator', () => { + const setup = () => { + const managementDefinition = new ManagementAppLocatorDefinition(); + const definition = new IngestPipelinesLocatorDefinition({ + managementAppLocator: { + getLocation: (params) => managementDefinition.getLocation(params), + getUrl: async () => { + throw new Error('not implemented'); + }, + navigate: async () => { + throw new Error('not implemented'); + }, + useUrl: () => '', + }, + }); + return { definition }; + }; + + describe('Pipelines List', () => { + it('generates relative url for list without pipelineId', async () => { + const { definition } = setup(); + const location = await definition.getLocation({ + page: INGEST_PIPELINES_PAGES.LIST, + }); + + expect(location).toMatchObject({ + app: 'management', + path: '/ingest/ingest_pipelines', + }); + }); + + it('generates relative url for list with a pipelineId', async () => { + const { definition } = setup(); + const location = await definition.getLocation({ + page: INGEST_PIPELINES_PAGES.LIST, + pipelineId: 'pipeline_name', + }); + + expect(location).toMatchObject({ + app: 'management', + path: '/ingest/ingest_pipelines/?pipeline=pipeline_name', + }); + }); + }); + + describe('Pipeline Edit', () => { + it('generates relative url for pipeline edit', async () => { + const { definition } = setup(); + const location = await definition.getLocation({ + page: INGEST_PIPELINES_PAGES.EDIT, + pipelineId: 'pipeline_name', + }); + + expect(location).toMatchObject({ + app: 'management', + path: '/ingest/ingest_pipelines/edit/pipeline_name', + }); + }); + }); + + describe('Pipeline Clone', () => { + it('generates relative url for pipeline clone', async () => { + const { definition } = setup(); + const location = await definition.getLocation({ + page: INGEST_PIPELINES_PAGES.CLONE, + pipelineId: 'pipeline_name', + }); + + expect(location).toMatchObject({ + app: 'management', + path: '/ingest/ingest_pipelines/create/pipeline_name', + }); + }); + }); + + describe('Pipeline Create', () => { + it('generates relative url for pipeline create', async () => { + const { definition } = setup(); + const location = await definition.getLocation({ + page: INGEST_PIPELINES_PAGES.CREATE, + pipelineId: 'pipeline_name', + }); + + expect(location).toMatchObject({ + app: 'management', + path: '/ingest/ingest_pipelines/create', + }); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/locator.ts b/x-pack/plugins/ingest_pipelines/public/locator.ts new file mode 100644 index 0000000000000..d819011f14f47 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/locator.ts @@ -0,0 +1,102 @@ +/* + * Copyright 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 { SerializableState } from 'src/plugins/kibana_utils/common'; +import { ManagementAppLocator } from 'src/plugins/management/common'; +import { + LocatorPublic, + LocatorDefinition, + KibanaLocation, +} from '../../../../src/plugins/share/public'; +import { + getClonePath, + getCreatePath, + getEditPath, + getListPath, +} from './application/services/navigation'; +import { PLUGIN_ID } from '../common/constants'; + +export enum INGEST_PIPELINES_PAGES { + LIST = 'pipelines_list', + EDIT = 'pipeline_edit', + CREATE = 'pipeline_create', + CLONE = 'pipeline_clone', +} + +interface IngestPipelinesBaseParams extends SerializableState { + pipelineId: string; +} +export interface IngestPipelinesListParams extends Partial { + page: INGEST_PIPELINES_PAGES.LIST; +} + +export interface IngestPipelinesEditParams extends IngestPipelinesBaseParams { + page: INGEST_PIPELINES_PAGES.EDIT; +} + +export interface IngestPipelinesCloneParams extends IngestPipelinesBaseParams { + page: INGEST_PIPELINES_PAGES.CLONE; +} + +export interface IngestPipelinesCreateParams extends IngestPipelinesBaseParams { + page: INGEST_PIPELINES_PAGES.CREATE; +} + +export type IngestPipelinesParams = + | IngestPipelinesListParams + | IngestPipelinesEditParams + | IngestPipelinesCloneParams + | IngestPipelinesCreateParams; + +export type IngestPipelinesLocator = LocatorPublic; + +export const INGEST_PIPELINES_APP_LOCATOR = 'INGEST_PIPELINES_APP_LOCATOR'; + +export interface IngestPipelinesLocatorDependencies { + managementAppLocator: ManagementAppLocator; +} + +export class IngestPipelinesLocatorDefinition implements LocatorDefinition { + public readonly id = INGEST_PIPELINES_APP_LOCATOR; + + constructor(protected readonly deps: IngestPipelinesLocatorDependencies) {} + + public readonly getLocation = async (params: IngestPipelinesParams): Promise => { + const location = await this.deps.managementAppLocator.getLocation({ + sectionId: 'ingest', + appId: PLUGIN_ID, + }); + + let path: string = ''; + + switch (params.page) { + case INGEST_PIPELINES_PAGES.EDIT: + path = getEditPath({ + pipelineName: params.pipelineId, + }); + break; + case INGEST_PIPELINES_PAGES.CREATE: + path = getCreatePath(); + break; + case INGEST_PIPELINES_PAGES.LIST: + path = getListPath({ + inspectedPipelineName: params.pipelineId, + }); + break; + case INGEST_PIPELINES_PAGES.CLONE: + path = getClonePath({ + clonedPipelineName: params.pipelineId, + }); + break; + } + + return { + ...location, + path: path === '/' ? location.path : location.path + path, + }; + }; +} diff --git a/x-pack/plugins/ingest_pipelines/public/plugin.ts b/x-pack/plugins/ingest_pipelines/public/plugin.ts index 4a138a12d6819..b4eb33162a1f4 100644 --- a/x-pack/plugins/ingest_pipelines/public/plugin.ts +++ b/x-pack/plugins/ingest_pipelines/public/plugin.ts @@ -11,7 +11,7 @@ import { CoreSetup, Plugin } from 'src/core/public'; import { PLUGIN_ID } from '../common/constants'; import { uiMetricService, apiService } from './application/services'; import { SetupDependencies, StartDependencies } from './types'; -import { registerUrlGenerator } from './url_generator'; +import { IngestPipelinesLocatorDefinition } from './locator'; export class IngestPipelinesPlugin implements Plugin { @@ -50,7 +50,11 @@ export class IngestPipelinesPlugin }, }); - registerUrlGenerator(coreSetup, management, share); + share.url.locators.create( + new IngestPipelinesLocatorDefinition({ + managementAppLocator: management.locator, + }) + ); } public start() {} diff --git a/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts b/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts deleted file mode 100644 index dc45f9bc39088..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { IngestPipelinesUrlGenerator, INGEST_PIPELINES_PAGES } from './url_generator'; - -describe('IngestPipelinesUrlGenerator', () => { - const getAppBasePath = (absolute: boolean = false) => { - if (absolute) { - return Promise.resolve('http://localhost/app/test_app'); - } - return Promise.resolve('/app/test_app'); - }; - const urlGenerator = new IngestPipelinesUrlGenerator(getAppBasePath); - - describe('Pipelines List', () => { - it('generates relative url for list without pipelineId', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.LIST, - }); - expect(url).toBe('/app/test_app/'); - }); - - it('generates absolute url for list without pipelineId', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.LIST, - absolute: true, - }); - expect(url).toBe('http://localhost/app/test_app/'); - }); - it('generates relative url for list with a pipelineId', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.LIST, - pipelineId: 'pipeline_name', - }); - expect(url).toBe('/app/test_app/?pipeline=pipeline_name'); - }); - - it('generates absolute url for list with a pipelineId', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.LIST, - pipelineId: 'pipeline_name', - absolute: true, - }); - expect(url).toBe('http://localhost/app/test_app/?pipeline=pipeline_name'); - }); - }); - - describe('Pipeline Edit', () => { - it('generates relative url for pipeline edit', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.EDIT, - pipelineId: 'pipeline_name', - }); - expect(url).toBe('/app/test_app/edit/pipeline_name'); - }); - - it('generates absolute url for pipeline edit', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.EDIT, - pipelineId: 'pipeline_name', - absolute: true, - }); - expect(url).toBe('http://localhost/app/test_app/edit/pipeline_name'); - }); - }); - - describe('Pipeline Clone', () => { - it('generates relative url for pipeline clone', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.CLONE, - pipelineId: 'pipeline_name', - }); - expect(url).toBe('/app/test_app/create/pipeline_name'); - }); - - it('generates absolute url for pipeline clone', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.CLONE, - pipelineId: 'pipeline_name', - absolute: true, - }); - expect(url).toBe('http://localhost/app/test_app/create/pipeline_name'); - }); - }); - - describe('Pipeline Create', () => { - it('generates relative url for pipeline create', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.CREATE, - pipelineId: 'pipeline_name', - }); - expect(url).toBe('/app/test_app/create'); - }); - - it('generates absolute url for pipeline create', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.CREATE, - pipelineId: 'pipeline_name', - absolute: true, - }); - expect(url).toBe('http://localhost/app/test_app/create'); - }); - }); -}); diff --git a/x-pack/plugins/ingest_pipelines/public/url_generator.ts b/x-pack/plugins/ingest_pipelines/public/url_generator.ts deleted file mode 100644 index d9a77addcd5fd..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/url_generator.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CoreSetup } from 'src/core/public'; -import { MANAGEMENT_APP_ID } from '../../../../src/plugins/management/public'; -import { UrlGeneratorsDefinition } from '../../../../src/plugins/share/public'; -import { - getClonePath, - getCreatePath, - getEditPath, - getListPath, -} from './application/services/navigation'; -import { SetupDependencies } from './types'; -import { PLUGIN_ID } from '../common/constants'; - -export const INGEST_PIPELINES_APP_ULR_GENERATOR = 'INGEST_PIPELINES_APP_URL_GENERATOR'; - -export enum INGEST_PIPELINES_PAGES { - LIST = 'pipelines_list', - EDIT = 'pipeline_edit', - CREATE = 'pipeline_create', - CLONE = 'pipeline_clone', -} - -interface UrlGeneratorState { - pipelineId: string; - absolute?: boolean; -} -export interface PipelinesListUrlGeneratorState extends Partial { - page: INGEST_PIPELINES_PAGES.LIST; -} - -export interface PipelineEditUrlGeneratorState extends UrlGeneratorState { - page: INGEST_PIPELINES_PAGES.EDIT; -} - -export interface PipelineCloneUrlGeneratorState extends UrlGeneratorState { - page: INGEST_PIPELINES_PAGES.CLONE; -} - -export interface PipelineCreateUrlGeneratorState extends UrlGeneratorState { - page: INGEST_PIPELINES_PAGES.CREATE; -} - -export type IngestPipelinesUrlGeneratorState = - | PipelinesListUrlGeneratorState - | PipelineEditUrlGeneratorState - | PipelineCloneUrlGeneratorState - | PipelineCreateUrlGeneratorState; - -export class IngestPipelinesUrlGenerator - implements UrlGeneratorsDefinition { - constructor(private readonly getAppBasePath: (absolute: boolean) => Promise) {} - - public readonly id = INGEST_PIPELINES_APP_ULR_GENERATOR; - - public readonly createUrl = async (state: IngestPipelinesUrlGeneratorState): Promise => { - switch (state.page) { - case INGEST_PIPELINES_PAGES.EDIT: { - return `${await this.getAppBasePath(!!state.absolute)}${getEditPath({ - pipelineName: state.pipelineId, - })}`; - } - case INGEST_PIPELINES_PAGES.CREATE: { - return `${await this.getAppBasePath(!!state.absolute)}${getCreatePath()}`; - } - case INGEST_PIPELINES_PAGES.LIST: { - return `${await this.getAppBasePath(!!state.absolute)}${getListPath({ - inspectedPipelineName: state.pipelineId, - })}`; - } - case INGEST_PIPELINES_PAGES.CLONE: { - return `${await this.getAppBasePath(!!state.absolute)}${getClonePath({ - clonedPipelineName: state.pipelineId, - })}`; - } - } - }; -} - -export const registerUrlGenerator = ( - coreSetup: CoreSetup, - management: SetupDependencies['management'], - share: SetupDependencies['share'] -) => { - const getAppBasePath = async (absolute = false) => { - const [coreStart] = await coreSetup.getStartServices(); - return coreStart.application.getUrlForApp(MANAGEMENT_APP_ID, { - path: management.sections.section.ingest.getApp(PLUGIN_ID)!.basePath, - absolute: !!absolute, - }); - }; - - share.urlGenerators.registerUrlGenerator(new IngestPipelinesUrlGenerator(getAppBasePath)); -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx index 88ffaa0da7fdc..93be45bbdaf97 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx @@ -114,10 +114,7 @@ export const ExpandedRow: FC = ({ item }) => { } const { - services: { - share, - application: { navigateToUrl }, - }, + services: { share }, } = useMlKibana(); const tabs = [ @@ -402,17 +399,16 @@ export const ExpandedRow: FC = ({ item }) => { { - const ingestPipelinesAppUrlGenerator = share.urlGenerators.getUrlGenerator( - 'INGEST_PIPELINES_APP_URL_GENERATOR' - ); - await navigateToUrl( - await ingestPipelinesAppUrlGenerator.createUrl({ - page: 'pipeline_edit', - pipelineId: pipelineName, - absolute: true, - }) + onClick={() => { + const locator = share.url.locators.get( + 'INGEST_PIPELINES_APP_LOCATOR' ); + if (!locator) return; + locator.navigate({ + page: 'pipeline_edit', + pipelineId: pipelineName, + absolute: true, + }); }} > Date: Wed, 23 Jun 2021 06:31:51 -0400 Subject: [PATCH 02/43] [Lens] Avoid suggestion rendering and evaluation on fullscreen mode (#102757) (#103054) Co-authored-by: Marco Liberati --- .../editor_frame/editor_frame.test.tsx | 51 +++++++++++++++++++ .../editor_frame/editor_frame.tsx | 3 +- .../editor_frame/frame_layout.scss | 5 +- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 52488cb32ae83..0e2ba5ce8ad59 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -1370,6 +1370,57 @@ describe('editor_frame', () => { }) ); }); + + it('should avoid completely to compute suggestion when in fullscreen mode', async () => { + const props = { + ...getDefaultProps(), + initialContext: { + indexPatternId: '1', + fieldName: 'test', + }, + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + testDatasource2: mockDatasource2, + }, + + ExpressionRenderer: expressionRendererMock, + }; + + const { instance: el } = await mountWithProvider( + , + props.plugins.data + ); + instance = el; + + expect( + instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement + ).not.toBeUndefined(); + + await act(async () => { + (instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({ + type: 'TOGGLE_FULLSCREEN', + }); + }); + + instance.update(); + + expect(instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement).toBe(false); + + await act(async () => { + (instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({ + type: 'TOGGLE_FULLSCREEN', + }); + }); + + instance.update(); + + expect( + instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement + ).not.toBeUndefined(); + }); }); describe('passing state back to the caller', () => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index cc65bb126d2d9..bd96682f427fa 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -452,7 +452,8 @@ export function EditorFrame(props: EditorFrameProps) { ) } suggestionsPanel={ - allLoaded && ( + allLoaded && + !state.isFullscreenDatasource && ( Date: Wed, 23 Jun 2021 06:33:13 -0400 Subject: [PATCH 03/43] [Lens] Remove rank direction tooltip (#102886) (#103052) Co-authored-by: Marco Liberati --- .../operations/definitions/terms/index.tsx | 22 +++---------------- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 3 files changed, 3 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index 7551b88039182..a650c556c4965 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -424,25 +424,9 @@ export const termsOperation: OperationDefinition - {i18n.translate('xpack.lens.indexPattern.terms.orderDirection', { - defaultMessage: 'Rank direction', - })}{' '} - - - } + label={i18n.translate('xpack.lens.indexPattern.terms.orderDirection', { + defaultMessage: 'Rank direction', + })} display="columnCompressed" fullWidth > diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index efffd2b6d7b0f..b722682616843 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12743,7 +12743,6 @@ "xpack.lens.indexPattern.terms.orderByHelp": "上位の値がランク付けされる条件となるディメンションを指定します。", "xpack.lens.indexPattern.terms.orderDescending": "降順", "xpack.lens.indexPattern.terms.orderDirection": "ランク方向", - "xpack.lens.indexPattern.terms.orderDirectionHelp": "上位の値のランク順序を指定します。", "xpack.lens.indexPattern.terms.otherBucketDescription": "他の値を「その他」としてグループ化", "xpack.lens.indexPattern.terms.otherLabel": "その他", "xpack.lens.indexPattern.terms.size": "値の数", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d2916c4ef8704..7e16125888d8d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12914,7 +12914,6 @@ "xpack.lens.indexPattern.terms.orderByHelp": "指定排名靠前值排名所依据的维度。", "xpack.lens.indexPattern.terms.orderDescending": "降序", "xpack.lens.indexPattern.terms.orderDirection": "排名方向", - "xpack.lens.indexPattern.terms.orderDirectionHelp": "指定排名靠前值的排名顺序。", "xpack.lens.indexPattern.terms.otherBucketDescription": "将其他值分组为“其他”", "xpack.lens.indexPattern.terms.otherLabel": "其他", "xpack.lens.indexPattern.terms.size": "值数目", From 227e5bbb3c9f7c1cfab46abe75e1d0043eae8454 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 07:18:46 -0400 Subject: [PATCH 04/43] [OsQuery] fix usage collector when .fleet indices are empty (#102977) (#103058) Co-authored-by: Nicolas Chaulet --- x-pack/plugins/osquery/server/usage/fetchers.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/plugins/osquery/server/usage/fetchers.ts b/x-pack/plugins/osquery/server/usage/fetchers.ts index 5f5b282331bec..3142b72906898 100644 --- a/x-pack/plugins/osquery/server/usage/fetchers.ts +++ b/x-pack/plugins/osquery/server/usage/fetchers.ts @@ -55,6 +55,7 @@ export async function getPolicyLevelUsage( }, }, index: '.fleet-agents', + ignore_unavailable: true, }); const policied = agentResponse.body.aggregations?.policied as AggregationsSingleBucketAggregate; if (policied && typeof policied.doc_count === 'number') { @@ -117,6 +118,7 @@ export async function getLiveQueryUsage( }, }, index: '.fleet-actions', + ignore_unavailable: true, }); const result: LiveQueryUsage = { session: await getRouteMetric(soClient, 'live_query'), @@ -185,6 +187,7 @@ export async function getBeatUsage(esClient: ElasticsearchClient) { }, }, index: METRICS_INDICES, + ignore_unavailable: true, }); const lastDayAggs = metricResponse.aggregations?.lastDay as AggregationsSingleBucketAggregate; const result: BeatMetricsUsage = { From 8cf034bcbfcca202b0d1e78fc551ef9687b856c1 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 08:14:17 -0400 Subject: [PATCH 05/43] [cli] Add kibana-encryption-keys.bat (#102070) (#103065) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Jonathan Budzenski --- .../bin/scripts/kibana-encryption-keys.bat | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100755 src/dev/build/tasks/bin/scripts/kibana-encryption-keys.bat diff --git a/src/dev/build/tasks/bin/scripts/kibana-encryption-keys.bat b/src/dev/build/tasks/bin/scripts/kibana-encryption-keys.bat new file mode 100755 index 0000000000000..9221af3142e61 --- /dev/null +++ b/src/dev/build/tasks/bin/scripts/kibana-encryption-keys.bat @@ -0,0 +1,35 @@ +@echo off + +SETLOCAL ENABLEDELAYEDEXPANSION + +set SCRIPT_DIR=%~dp0 +for %%I in ("%SCRIPT_DIR%..") do set DIR=%%~dpfI + +set NODE=%DIR%\node\node.exe + +If Not Exist "%NODE%" ( + Echo unable to find usable node.js executable. + Exit /B 1 +) + +set CONFIG_DIR=%KBN_PATH_CONF% +If [%KBN_PATH_CONF%] == [] ( + set "CONFIG_DIR=%DIR%\config" +) + +IF EXIST "%CONFIG_DIR%\node.options" ( + for /F "usebackq eol=# tokens=*" %%i in ("%CONFIG_DIR%\node.options") do ( + If [!NODE_OPTIONS!] == [] ( + set "NODE_OPTIONS=%%i" + ) Else ( + set "NODE_OPTIONS=!NODE_OPTIONS! %%i" + ) + ) +) + +TITLE Kibana Encryption Keys +"%NODE%" "%DIR%\src\cli_encryption_keys\dist" %* + +:finally + +ENDLOCAL From 9e2b89d4e7c07bab680878ee8c81da0d34aa82d4 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Wed, 23 Jun 2021 15:32:57 +0200 Subject: [PATCH 06/43] Discover locator (#102712) (#103072) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Discover locator * Add Discover locator tests * Expose locator for Discover app and deprecate URL generator * Use Discover locator in Explore Underlying Data * Fix explore data unit tests after refactor * fix: 🐛 update Discover plugin mock * style: 💄 remove any * test: 💍 fix test mock * fix: 🐛 adjust property name after refactor * test: 💍 fix tests after refactor Co-authored-by: Vadim Kibana Co-authored-by: Vadim Kibana --- src/plugins/discover/public/index.ts | 2 + src/plugins/discover/public/locator.test.ts | 270 ++++++++++++++++++ src/plugins/discover/public/locator.ts | 146 ++++++++++ src/plugins/discover/public/mocks.ts | 12 + src/plugins/discover/public/plugin.tsx | 76 ++++- x-pack/plugins/discover_enhanced/kibana.json | 2 +- .../abstract_explore_data_action.ts | 22 +- .../explore_data_chart_action.test.ts | 46 ++- .../explore_data/explore_data_chart_action.ts | 28 +- .../explore_data_context_menu_action.test.ts | 42 ++- .../explore_data_context_menu_action.ts | 28 +- 11 files changed, 586 insertions(+), 88 deletions(-) create mode 100644 src/plugins/discover/public/locator.test.ts create mode 100644 src/plugins/discover/public/locator.ts diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index fbe853ec6deb5..3840df4353faf 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -17,4 +17,6 @@ export function plugin(initializerContext: PluginInitializerContext) { export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './saved_searches'; export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable'; export { loadSharingDataHelpers } from './shared'; + export { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from './url_generator'; +export { DiscoverAppLocator, DiscoverAppLocatorParams } from './locator'; diff --git a/src/plugins/discover/public/locator.test.ts b/src/plugins/discover/public/locator.test.ts new file mode 100644 index 0000000000000..edbb0663d4aa3 --- /dev/null +++ b/src/plugins/discover/public/locator.test.ts @@ -0,0 +1,270 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { hashedItemStore, getStatesFromKbnUrl } from '../../kibana_utils/public'; +import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; +import { FilterStateStore } from '../../data/common'; +import { DiscoverAppLocatorDefinition } from './locator'; +import { SerializableState } from 'src/plugins/kibana_utils/common'; + +const indexPatternId: string = 'c367b774-a4c2-11ea-bb37-0242ac130002'; +const savedSearchId: string = '571aaf70-4c88-11e8-b3d7-01146121b73d'; + +interface SetupParams { + useHash?: boolean; +} + +const setup = async ({ useHash = false }: SetupParams = {}) => { + const locator = new DiscoverAppLocatorDefinition({ + useHash, + }); + + return { + locator, + }; +}; + +beforeEach(() => { + // @ts-expect-error + hashedItemStore.storage = mockStorage; +}); + +describe('Discover url generator', () => { + test('can create a link to Discover with no state and no saved search', async () => { + const { locator } = await setup(); + const { app, path } = await locator.getLocation({}); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(app).toBe('discover'); + expect(_a).toEqual({}); + expect(_g).toEqual({}); + }); + + test('can create a link to a saved search in Discover', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ savedSearchId }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(path.startsWith(`#/view/${savedSearchId}`)).toBe(true); + expect(_a).toEqual({}); + expect(_g).toEqual({}); + }); + + test('can specify specific index pattern', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + indexPatternId, + }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(_a).toEqual({ + index: indexPatternId, + }); + expect(_g).toEqual({}); + }); + + test('can specify specific time range', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, + }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(_a).toEqual({}); + expect(_g).toEqual({ + time: { + from: 'now-15m', + mode: 'relative', + to: 'now', + }, + }); + }); + + test('can specify query', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + query: { + language: 'kuery', + query: 'foo', + }, + }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(_a).toEqual({ + query: { + language: 'kuery', + query: 'foo', + }, + }); + expect(_g).toEqual({}); + }); + + test('can specify local and global filters', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + filters: [ + { + meta: { + alias: 'foo', + disabled: false, + negate: false, + }, + $state: { + store: FilterStateStore.APP_STATE, + }, + }, + { + meta: { + alias: 'bar', + disabled: false, + negate: false, + }, + $state: { + store: FilterStateStore.GLOBAL_STATE, + }, + }, + ], + }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(_a).toEqual({ + filters: [ + { + $state: { + store: 'appState', + }, + meta: { + alias: 'foo', + disabled: false, + negate: false, + }, + }, + ], + }); + expect(_g).toEqual({ + filters: [ + { + $state: { + store: 'globalState', + }, + meta: { + alias: 'bar', + disabled: false, + negate: false, + }, + }, + ], + }); + }); + + test('can set refresh interval', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + refreshInterval: { + pause: false, + value: 666, + }, + }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(_a).toEqual({}); + expect(_g).toEqual({ + refreshInterval: { + pause: false, + value: 666, + }, + }); + }); + + test('can set time range', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + timeRange: { + from: 'now-3h', + to: 'now', + }, + }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(_a).toEqual({}); + expect(_g).toEqual({ + time: { + from: 'now-3h', + to: 'now', + }, + }); + }); + + test('can specify a search session id', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + searchSessionId: '__test__', + }); + + expect(path).toMatchInlineSnapshot(`"#/?_g=()&_a=()&searchSessionId=__test__"`); + expect(path).toContain('__test__'); + }); + + test('can specify columns, interval, sort and savedQuery', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + columns: ['_source'], + interval: 'auto', + sort: [['timestamp, asc']] as string[][] & SerializableState, + savedQuery: '__savedQueryId__', + }); + + expect(path).toMatchInlineSnapshot( + `"#/?_g=()&_a=(columns:!(_source),interval:auto,savedQuery:__savedQueryId__,sort:!(!('timestamp,%20asc')))"` + ); + }); + + describe('useHash property', () => { + describe('when default useHash is set to false', () => { + test('when using default, sets index pattern ID in the generated URL', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + indexPatternId, + }); + + expect(path.indexOf(indexPatternId) > -1).toBe(true); + }); + + test('when enabling useHash, does not set index pattern ID in the generated URL', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + useHash: true, + indexPatternId, + }); + + expect(path.indexOf(indexPatternId) > -1).toBe(false); + }); + }); + + describe('when default useHash is set to true', () => { + test('when using default, does not set index pattern ID in the generated URL', async () => { + const { locator } = await setup({ useHash: true }); + const { path } = await locator.getLocation({ + indexPatternId, + }); + + expect(path.indexOf(indexPatternId) > -1).toBe(false); + }); + + test('when disabling useHash, sets index pattern ID in the generated URL', async () => { + const { locator } = await setup({ useHash: true }); + const { path } = await locator.getLocation({ + useHash: false, + indexPatternId, + }); + + expect(path.indexOf(indexPatternId) > -1).toBe(true); + }); + }); + }); +}); diff --git a/src/plugins/discover/public/locator.ts b/src/plugins/discover/public/locator.ts new file mode 100644 index 0000000000000..fff89903bc465 --- /dev/null +++ b/src/plugins/discover/public/locator.ts @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public'; +import type { LocatorDefinition, LocatorPublic } from '../../share/public'; +import { esFilters } from '../../data/public'; +import { setStateToKbnUrl } from '../../kibana_utils/public'; + +export const DISCOVER_APP_LOCATOR = 'DISCOVER_APP_LOCATOR'; + +export interface DiscoverAppLocatorParams extends SerializableState { + /** + * Optionally set saved search ID. + */ + savedSearchId?: string; + + /** + * Optionally set index pattern ID. + */ + indexPatternId?: string; + + /** + * Optionally set the time range in the time picker. + */ + timeRange?: TimeRange; + + /** + * Optionally set the refresh interval. + */ + refreshInterval?: RefreshInterval & SerializableState; + + /** + * Optionally apply filters. + */ + filters?: Filter[]; + + /** + * Optionally set a query. + */ + query?: Query; + + /** + * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines + * whether to hash the data in the url to avoid url length issues. + */ + useHash?: boolean; + + /** + * Background search session id + */ + searchSessionId?: string; + + /** + * Columns displayed in the table + */ + columns?: string[]; + + /** + * Used interval of the histogram + */ + interval?: string; + + /** + * Array of the used sorting [[field,direction],...] + */ + sort?: string[][] & SerializableState; + + /** + * id of the used saved query + */ + savedQuery?: string; +} + +export type DiscoverAppLocator = LocatorPublic; + +export interface DiscoverAppLocatorDependencies { + useHash: boolean; +} + +export class DiscoverAppLocatorDefinition implements LocatorDefinition { + public readonly id = DISCOVER_APP_LOCATOR; + + constructor(protected readonly deps: DiscoverAppLocatorDependencies) {} + + public readonly getLocation = async (params: DiscoverAppLocatorParams) => { + const { + useHash = this.deps.useHash, + filters, + indexPatternId, + query, + refreshInterval, + savedSearchId, + timeRange, + searchSessionId, + columns, + savedQuery, + sort, + interval, + } = params; + const savedSearchPath = savedSearchId ? `view/${encodeURIComponent(savedSearchId)}` : ''; + const appState: { + query?: Query; + filters?: Filter[]; + index?: string; + columns?: string[]; + interval?: string; + sort?: string[][]; + savedQuery?: string; + } = {}; + const queryState: QueryState = {}; + + if (query) appState.query = query; + if (filters && filters.length) + appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f)); + if (indexPatternId) appState.index = indexPatternId; + if (columns) appState.columns = columns; + if (savedQuery) appState.savedQuery = savedQuery; + if (sort) appState.sort = sort; + if (interval) appState.interval = interval; + + if (timeRange) queryState.time = timeRange; + if (filters && filters.length) + queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f)); + if (refreshInterval) queryState.refreshInterval = refreshInterval; + + let path = `#/${savedSearchPath}`; + path = setStateToKbnUrl('_g', queryState, { useHash }, path); + path = setStateToKbnUrl('_a', appState, { useHash }, path); + + if (searchSessionId) { + path = `${path}&searchSessionId=${searchSessionId}`; + } + + return { + app: 'discover', + path, + state: {}, + }; + }; +} diff --git a/src/plugins/discover/public/mocks.ts b/src/plugins/discover/public/mocks.ts index 0f57c5c0fa138..53160df472a3c 100644 --- a/src/plugins/discover/public/mocks.ts +++ b/src/plugins/discover/public/mocks.ts @@ -16,6 +16,12 @@ const createSetupContract = (): Setup => { docViews: { addDocView: jest.fn(), }, + locator: { + getLocation: jest.fn(), + getUrl: jest.fn(), + useUrl: jest.fn(), + navigate: jest.fn(), + }, }; return setupContract; }; @@ -26,6 +32,12 @@ const createStartContract = (): Start => { urlGenerator: ({ createUrl: jest.fn(), } as unknown) as DiscoverStart['urlGenerator'], + locator: { + getLocation: jest.fn(), + getUrl: jest.fn(), + useUrl: jest.fn(), + navigate: jest.fn(), + }, }; return startContract; }; diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 7b4e7bb67c00e..ec89f7516e92d 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -59,6 +59,7 @@ import { DiscoverUrlGenerator, SEARCH_SESSION_ID_QUERY_PARAM, } from './url_generator'; +import { DiscoverAppLocatorDefinition, DiscoverAppLocator } from './locator'; import { SearchEmbeddableFactory } from './application/embeddable'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { replaceUrlHashQuery } from '../../kibana_utils/public/'; @@ -83,17 +84,68 @@ export interface DiscoverSetup { */ addDocView(docViewRaw: DocViewInput | DocViewInputFn): void; }; + + /** + * `share` plugin URL locator for Discover app. Use it to generate links into + * Discover application, for example, navigate: + * + * ```ts + * await plugins.discover.locator.navigate({ + * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d', + * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002', + * timeRange: { + * to: 'now', + * from: 'now-15m', + * mode: 'relative', + * }, + * }); + * ``` + * + * Generate a location: + * + * ```ts + * const location = await plugins.discover.locator.getLocation({ + * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d', + * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002', + * timeRange: { + * to: 'now', + * from: 'now-15m', + * mode: 'relative', + * }, + * }); + * ``` + */ + readonly locator: undefined | DiscoverAppLocator; } export interface DiscoverStart { savedSearchLoader: SavedObjectLoader; /** - * `share` plugin URL generator for Discover app. Use it to generate links into - * Discover application, example: + * @deprecated Use URL locator instead. URL generaotr will be removed. + */ + readonly urlGenerator: undefined | UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; + + /** + * `share` plugin URL locator for Discover app. Use it to generate links into + * Discover application, for example, navigate: + * + * ```ts + * await plugins.discover.locator.navigate({ + * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d', + * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002', + * timeRange: { + * to: 'now', + * from: 'now-15m', + * mode: 'relative', + * }, + * }); + * ``` + * + * Generate a location: * * ```ts - * const url = await plugins.discover.urlGenerator.createUrl({ + * const location = await plugins.discover.locator.getLocation({ * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d', * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002', * timeRange: { @@ -104,7 +156,7 @@ export interface DiscoverStart { * }); * ``` */ - readonly urlGenerator: undefined | UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; + readonly locator: undefined | DiscoverAppLocator; } /** @@ -156,7 +208,12 @@ export class DiscoverPlugin private stopUrlTracking: (() => void) | undefined = undefined; private servicesInitialized: boolean = false; private innerAngularInitialized: boolean = false; + + /** + * @deprecated + */ private urlGenerator?: DiscoverStart['urlGenerator']; + private locator?: DiscoverAppLocator; /** * why are those functions public? they are needed for some mocha tests @@ -179,6 +236,15 @@ export class DiscoverPlugin }) ); } + + if (plugins.share) { + this.locator = plugins.share.url.locators.create( + new DiscoverAppLocatorDefinition({ + useHash: core.uiSettings.get('state:storeInSessionStorage'), + }) + ); + } + this.docViewsRegistry = new DocViewsRegistry(); setDocViewsRegistry(this.docViewsRegistry); this.docViewsRegistry.addDocView({ @@ -323,6 +389,7 @@ export class DiscoverPlugin docViews: { addDocView: this.docViewsRegistry.addDocView.bind(this.docViewsRegistry), }, + locator: this.locator, }; } @@ -367,6 +434,7 @@ export class DiscoverPlugin return { urlGenerator: this.urlGenerator, + locator: this.locator, savedSearchLoader: createSavedSearchesLoader({ savedObjectsClient: core.savedObjects.client, savedObjects: plugins.savedObjects, diff --git a/x-pack/plugins/discover_enhanced/kibana.json b/x-pack/plugins/discover_enhanced/kibana.json index 01a3624d3e320..da95a0f21a020 100644 --- a/x-pack/plugins/discover_enhanced/kibana.json +++ b/x-pack/plugins/discover_enhanced/kibana.json @@ -7,5 +7,5 @@ "requiredPlugins": ["uiActions", "embeddable", "discover"], "optionalPlugins": ["share", "kibanaLegacy", "usageCollection"], "configPath": ["xpack", "discoverEnhanced"], - "requiredBundles": ["kibanaUtils", "data", "share"] + "requiredBundles": ["kibanaUtils", "data"] } diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts index 023db127ca633..44ea53fe0b870 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts @@ -11,13 +11,13 @@ import { ViewMode, IEmbeddable } from '../../../../../../src/plugins/embeddable/ import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public'; import { KibanaLegacyStart } from '../../../../../../src/plugins/kibana_legacy/public'; import { CoreStart } from '../../../../../../src/core/public'; -import { KibanaURL } from '../../../../../../src/plugins/share/public'; +import { KibanaLocation } from '../../../../../../src/plugins/share/public'; import * as shared from './shared'; export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA'; export interface PluginDeps { - discover: Pick; + discover: Pick; kibanaLegacy?: { dashboardConfig: { getHideWriteControls: KibanaLegacyStart['dashboardConfig']['getHideWriteControls']; @@ -26,7 +26,7 @@ export interface PluginDeps { } export interface CoreDeps { - application: Pick; + application: Pick; } export interface Params { @@ -43,7 +43,7 @@ export abstract class AbstractExploreDataAction; + protected abstract getLocation(context: Context): Promise; public async isCompatible({ embeddable }: Context): Promise { if (!embeddable) return false; @@ -52,7 +52,7 @@ export abstract class AbstractExploreDataAction { - type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; - const core = coreMock.createStart(); - - const urlGenerator: UrlGenerator = ({ - createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')), - } as unknown) as UrlGenerator; + const locator: DiscoverAppLocator = { + getLocation: jest.fn(() => + Promise.resolve({ + app: 'discover', + path: '/foo#bar', + state: {}, + }) + ), + navigate: jest.fn(async () => {}), + getUrl: jest.fn(), + useUrl: jest.fn(), + }; const plugins: PluginDeps = { discover: { - urlGenerator, + locator, }, kibanaLegacy: { dashboardConfig: { @@ -95,7 +101,7 @@ const setup = ( embeddable, } as ExploreDataChartActionContext; - return { core, plugins, urlGenerator, params, action, input, output, embeddable, context }; + return { core, plugins, locator, params, action, input, output, embeddable, context }; }; describe('"Explore underlying data" panel action', () => { @@ -132,7 +138,7 @@ describe('"Explore underlying data" panel action', () => { test('returns false when URL generator is not present', async () => { const { action, plugins, context } = setup(); - (plugins.discover as any).urlGenerator = undefined; + (plugins.discover as any).locator = undefined; const isCompatible = await action.isCompatible(context); @@ -205,23 +211,15 @@ describe('"Explore underlying data" panel action', () => { }); describe('getHref()', () => { - test('returns URL path generated by URL generator', async () => { - const { action, context } = setup(); - - const href = await action.getHref(context); - - expect(href).toBe('/xyz/app/discover/foo#bar'); - }); - test('calls URL generator with right arguments', async () => { - const { action, urlGenerator, context } = setup(); + const { action, locator, context } = setup(); - expect(urlGenerator.createUrl).toHaveBeenCalledTimes(0); + expect(locator.getLocation).toHaveBeenCalledTimes(0); await action.getHref(context); - expect(urlGenerator.createUrl).toHaveBeenCalledTimes(1); - expect(urlGenerator.createUrl).toHaveBeenCalledWith({ + expect(locator.getLocation).toHaveBeenCalledTimes(1); + expect(locator.getLocation).toHaveBeenCalledWith({ filters: [], indexPatternId: 'index-ptr-foo', timeRange: undefined, @@ -260,11 +258,11 @@ describe('"Explore underlying data" panel action', () => { }, ]; - const { action, context, urlGenerator } = setup({ filters, timeFieldName }); + const { action, context, locator } = setup({ filters, timeFieldName }); await action.getHref(context); - expect(urlGenerator.createUrl).toHaveBeenCalledWith({ + expect(locator.getLocation).toHaveBeenCalledWith({ filters: [ { meta: { diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts index 32264ee1deceb..7b59a4e51d042 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts @@ -7,7 +7,7 @@ import { Action } from '../../../../../../src/plugins/ui_actions/public'; import { - DiscoverUrlGeneratorState, + DiscoverAppLocatorParams, SearchInput, } from '../../../../../../src/plugins/discover/public'; import { @@ -15,7 +15,7 @@ import { esFilters, } from '../../../../../../src/plugins/data/public'; import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; -import { KibanaURL } from '../../../../../../src/plugins/share/public'; +import { KibanaLocation } from '../../../../../../src/plugins/share/public'; import * as shared from './shared'; import { AbstractExploreDataAction } from './abstract_explore_data_action'; @@ -43,14 +43,14 @@ export class ExploreDataChartAction return super.isCompatible(context); } - protected readonly getUrl = async ( + protected readonly getLocation = async ( context: ExploreDataChartActionContext - ): Promise => { + ): Promise => { const { plugins } = this.params.start(); - const { urlGenerator } = plugins.discover; + const { locator } = plugins.discover; - if (!urlGenerator) { - throw new Error('Discover URL generator not available.'); + if (!locator) { + throw new Error('Discover URL locator not available.'); } const { embeddable } = context; @@ -59,23 +59,23 @@ export class ExploreDataChartAction context.timeFieldName ); - const state: DiscoverUrlGeneratorState = { + const params: DiscoverAppLocatorParams = { filters, timeRange, }; if (embeddable) { - state.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined; + params.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined; const input = embeddable.getInput() as Readonly; - if (input.timeRange && !state.timeRange) state.timeRange = input.timeRange; - if (input.query) state.query = input.query; - if (input.filters) state.filters = [...input.filters, ...(state.filters || [])]; + if (input.timeRange && !params.timeRange) params.timeRange = input.timeRange; + if (input.query) params.query = input.query; + if (input.filters) params.filters = [...input.filters, ...(params.filters || [])]; } - const path = await urlGenerator.createUrl(state); + const location = await locator.getLocation(params); - return new KibanaURL(path); + return location; }; } diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts index 842c7d6b339b4..5bdac602ec271 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts @@ -8,13 +8,13 @@ import { ExploreDataContextMenuAction } from './explore_data_context_menu_action'; import { Params, PluginDeps } from './abstract_explore_data_action'; import { coreMock } from '../../../../../../src/core/public/mocks'; -import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public'; import { i18n } from '@kbn/i18n'; import { VisualizeEmbeddableContract, VISUALIZE_EMBEDDABLE_TYPE, } from '../../../../../../src/plugins/visualizations/public'; import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; +import { DiscoverAppLocator } from '../../../../../../src/plugins/discover/public'; const i18nTranslateSpy = (i18n.translate as unknown) as jest.SpyInstance; @@ -29,17 +29,23 @@ afterEach(() => { }); const setup = ({ dashboardOnlyMode = false }: { dashboardOnlyMode?: boolean } = {}) => { - type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; - const core = coreMock.createStart(); - - const urlGenerator: UrlGenerator = ({ - createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')), - } as unknown) as UrlGenerator; + const locator: DiscoverAppLocator = { + getLocation: jest.fn(() => + Promise.resolve({ + app: 'discover', + path: '/foo#bar', + state: {}, + }) + ), + navigate: jest.fn(async () => {}), + getUrl: jest.fn(), + useUrl: jest.fn(), + }; const plugins: PluginDeps = { discover: { - urlGenerator, + locator, }, kibanaLegacy: { dashboardConfig: { @@ -79,7 +85,7 @@ const setup = ({ dashboardOnlyMode = false }: { dashboardOnlyMode?: boolean } = embeddable, }; - return { core, plugins, urlGenerator, params, action, input, output, embeddable, context }; + return { core, plugins, locator, params, action, input, output, embeddable, context }; }; describe('"Explore underlying data" panel action', () => { @@ -116,7 +122,7 @@ describe('"Explore underlying data" panel action', () => { test('returns false when URL generator is not present', async () => { const { action, plugins, context } = setup(); - (plugins.discover as any).urlGenerator = undefined; + (plugins.discover as any).locator = undefined; const isCompatible = await action.isCompatible(context); @@ -189,23 +195,15 @@ describe('"Explore underlying data" panel action', () => { }); describe('getHref()', () => { - test('returns URL path generated by URL generator', async () => { - const { action, context } = setup(); - - const href = await action.getHref(context); - - expect(href).toBe('/xyz/app/discover/foo#bar'); - }); - test('calls URL generator with right arguments', async () => { - const { action, urlGenerator, context } = setup(); + const { action, locator, context } = setup(); - expect(urlGenerator.createUrl).toHaveBeenCalledTimes(0); + expect(locator.getLocation).toHaveBeenCalledTimes(0); await action.getHref(context); - expect(urlGenerator.createUrl).toHaveBeenCalledTimes(1); - expect(urlGenerator.createUrl).toHaveBeenCalledWith({ + expect(locator.getLocation).toHaveBeenCalledTimes(1); + expect(locator.getLocation).toHaveBeenCalledWith({ indexPatternId: 'index-ptr-foo', }); }); diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts index 99a2afd239645..88c093a299cb9 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts @@ -12,8 +12,8 @@ import { IEmbeddable, } from '../../../../../../src/plugins/embeddable/public'; import { Query, Filter, TimeRange } from '../../../../../../src/plugins/data/public'; -import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public'; -import { KibanaURL } from '../../../../../../src/plugins/share/public'; +import { DiscoverAppLocatorParams } from '../../../../../../src/plugins/discover/public'; +import { KibanaLocation } from '../../../../../../src/plugins/share/public'; import * as shared from './shared'; import { AbstractExploreDataAction } from './abstract_explore_data_action'; @@ -40,29 +40,31 @@ export class ExploreDataContextMenuAction public readonly order = 200; - protected readonly getUrl = async (context: EmbeddableQueryContext): Promise => { + protected readonly getLocation = async ( + context: EmbeddableQueryContext + ): Promise => { const { plugins } = this.params.start(); - const { urlGenerator } = plugins.discover; + const { locator } = plugins.discover; - if (!urlGenerator) { - throw new Error('Discover URL generator not available.'); + if (!locator) { + throw new Error('Discover URL locator not available.'); } const { embeddable } = context; - const state: DiscoverUrlGeneratorState = {}; + const params: DiscoverAppLocatorParams = {}; if (embeddable) { - state.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined; + params.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined; const input = embeddable.getInput(); - if (input.timeRange && !state.timeRange) state.timeRange = input.timeRange; - if (input.query) state.query = input.query; - if (input.filters) state.filters = [...input.filters, ...(state.filters || [])]; + if (input.timeRange && !params.timeRange) params.timeRange = input.timeRange; + if (input.query) params.query = input.query; + if (input.filters) params.filters = [...input.filters, ...(params.filters || [])]; } - const path = await urlGenerator.createUrl(state); + const location = await locator.getLocation(params); - return new KibanaURL(path); + return location; }; } From 03b880082c5d4759d387d971648be974b6c5ad81 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 09:49:21 -0400 Subject: [PATCH 07/43] fix time shift ux issues (#102709) (#103074) Co-authored-by: Joe Reuter --- .../search/aggs/utils/parse_time_shift.ts | 2 +- .../dimension_panel/time_shift.tsx | 2 +- .../time_shift_utils.tsx | 22 +++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts b/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts index 4d8ee0f889173..91379ea054de3 100644 --- a/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts +++ b/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts @@ -20,7 +20,7 @@ export const parseTimeShift = (val: string): moment.Duration | 'previous' | 'inv if (trimmedVal === 'previous') { return 'previous'; } - const [, amount, unit] = trimmedVal.match(/^(\d+)(\w)$/) || []; + const [, amount, unit] = trimmedVal.match(/^(\d+)\s*(\w)$/) || []; const parsedAmount = Number(amount); if (Number.isNaN(parsedAmount) || !allowedUnits.includes(unit as AllowedUnit)) { return 'invalid'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx index ba9525ac53fc5..c2415c9c9a75a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx @@ -157,7 +157,7 @@ export function TimeShift({ isClearable={false} data-test-subj="indexPattern-dimension-time-shift" placeholder={i18n.translate('xpack.lens.indexPattern.timeShiftPlaceholder', { - defaultMessage: 'Time shift (e.g. 1d)', + defaultMessage: 'Type custom values (e.g. 8w)', })} options={timeShiftOptions.filter(({ value }) => { const parsedValue = parseTimeShift(value); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx index 14ba6b9189e6b..a1bc643c3bd93 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx @@ -23,67 +23,67 @@ import { FramePublicAPI } from '../types'; export const timeShiftOptions = [ { label: i18n.translate('xpack.lens.indexPattern.timeShift.hour', { - defaultMessage: '1 hour (1h)', + defaultMessage: '1 hour ago (1h)', }), value: '1h', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.3hours', { - defaultMessage: '3 hours (3h)', + defaultMessage: '3 hours ago (3h)', }), value: '3h', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.6hours', { - defaultMessage: '6 hours (6h)', + defaultMessage: '6 hours ago (6h)', }), value: '6h', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.12hours', { - defaultMessage: '12 hours (12h)', + defaultMessage: '12 hours ago (12h)', }), value: '12h', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.day', { - defaultMessage: '1 day (1d)', + defaultMessage: '1 day ago (1d)', }), value: '1d', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.week', { - defaultMessage: '1 week (1w)', + defaultMessage: '1 week ago (1w)', }), value: '1w', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.month', { - defaultMessage: '1 month (1M)', + defaultMessage: '1 month ago (1M)', }), value: '1M', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.3months', { - defaultMessage: '3 months (3M)', + defaultMessage: '3 months ago (3M)', }), value: '3M', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.6months', { - defaultMessage: '6 months (6M)', + defaultMessage: '6 months ago (6M)', }), value: '6M', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.year', { - defaultMessage: '1 year (1y)', + defaultMessage: '1 year ago (1y)', }), value: '1y', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.previous', { - defaultMessage: 'Previous', + defaultMessage: 'Previous time range', }), value: 'previous', }, From e583c6d358a56305182d8cdbd268e5b47dde0a1d Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 09:52:10 -0400 Subject: [PATCH 08/43] disable missing switch for non-string fields (#102865) (#103073) Co-authored-by: Joe Reuter --- .../operations/definitions/terms/index.tsx | 5 ++- .../definitions/terms/terms.test.tsx | 32 +++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index a650c556c4965..a458a1edcfa16 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -497,7 +497,10 @@ export const termsOperation: OperationDefinition diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index 3b557461546ca..f326f3e3ed5f6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -60,7 +60,7 @@ describe('terms', () => { size: 3, orderDirection: 'asc', }, - sourceField: 'category', + sourceField: 'source', }, col2: { label: 'Count', @@ -88,7 +88,7 @@ describe('terms', () => { expect.objectContaining({ arguments: expect.objectContaining({ orderBy: ['_key'], - field: ['category'], + field: ['source'], size: [3], otherBucket: [true], }), @@ -770,6 +770,34 @@ describe('terms', () => { expect(select.prop('disabled')).toEqual(false); }); + it('should disable missing bucket setting if field is not a string', () => { + const updateLayerSpy = jest.fn(); + const instance = shallow( + + ); + + const select = instance + .find('[data-test-subj="indexPattern-terms-missing-bucket"]') + .find(EuiSwitch); + + expect(select.prop('disabled')).toEqual(true); + }); + it('should update state when clicking other bucket toggle', () => { const updateLayerSpy = jest.fn(); const instance = shallow( From 797c29c2d5f757c03ce5a95b36303dd0fc9990a2 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 09:53:02 -0400 Subject: [PATCH 09/43] [Lens] Do not add math columns for pass-through operations (#102656) (#103075) Co-authored-by: Joe Reuter --- .../definitions/calculations/utils.ts | 23 ++++++- .../definitions/formula/formula.test.tsx | 20 ++---- .../operations/definitions/formula/parse.ts | 52 ++++++++------- .../operations/layer_helpers.test.ts | 63 +++++++++---------- 4 files changed, 85 insertions(+), 73 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts index 03b9d6c07709c..87116f71919b5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts @@ -7,11 +7,12 @@ import { i18n } from '@kbn/i18n'; import type { ExpressionFunctionAST } from '@kbn/interpreter/common'; +import memoizeOne from 'memoize-one'; import type { TimeScaleUnit } from '../../../time_scale'; import type { IndexPattern, IndexPatternLayer } from '../../../types'; import { adjustTimeScaleLabelSuffix } from '../../time_scale_utils'; import type { ReferenceBasedIndexPatternColumn } from '../column_types'; -import { isColumnValidAsReference } from '../../layer_helpers'; +import { getManagedColumnsFrom, isColumnValidAsReference } from '../../layer_helpers'; import { operationDefinitionMap } from '..'; export const buildLabelFunction = (ofName: (name?: string) => string) => ( @@ -45,6 +46,23 @@ export function checkForDateHistogram(layer: IndexPatternLayer, name: string) { ]; } +const getFullyManagedColumnIds = memoizeOne((layer: IndexPatternLayer) => { + const managedColumnIds = new Set(); + Object.entries(layer.columns).forEach(([id, column]) => { + if ( + 'references' in column && + operationDefinitionMap[column.operationType].input === 'managedReference' + ) { + managedColumnIds.add(id); + const managedColumns = getManagedColumnsFrom(id, layer.columns); + managedColumns.map(([managedId]) => { + managedColumnIds.add(managedId); + }); + } + }); + return managedColumnIds; +}); + export function checkReferences(layer: IndexPatternLayer, columnId: string) { const column = layer.columns[columnId] as ReferenceBasedIndexPatternColumn; @@ -72,7 +90,8 @@ export function checkReferences(layer: IndexPatternLayer, columnId: string) { column: referenceColumn, }); - if (!isValid) { + // do not enforce column validity if current column is part of managed subtree + if (!isValid && !getFullyManagedColumnIds(layer).has(columnId)) { errors.push( i18n.translate('xpack.lens.indexPattern.invalidReferenceConfiguration', { defaultMessage: 'Dimension "{dimensionLabel}" is configured incorrectly', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx index e6aa29ea4d763..279e76b839548 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx @@ -413,13 +413,13 @@ describe('formula', () => { ).newLayer ).toEqual({ ...layer, - columnOrder: ['col1X0', 'col1X1', 'col1'], + columnOrder: ['col1X0', 'col1'], columns: { ...layer.columns, col1: { ...currentColumn, label: 'average(bytes)', - references: ['col1X1'], + references: ['col1X0'], params: { ...currentColumn.params, formula: 'average(bytes)', @@ -436,18 +436,6 @@ describe('formula', () => { sourceField: 'bytes', timeScale: false, }, - col1X1: { - customLabel: true, - dataType: 'number', - isBucketed: false, - label: 'Part of average(bytes)', - operationType: 'math', - params: { - tinymathAst: 'col1X0', - }, - references: ['col1X0'], - scale: 'ratio', - }, }, }); }); @@ -568,8 +556,8 @@ describe('formula', () => { ).locations ).toEqual({ col1X0: { min: 15, max: 29 }, - col1X2: { min: 0, max: 41 }, - col1X3: { min: 42, max: 50 }, + col1X1: { min: 0, max: 41 }, + col1X2: { min: 42, max: 50 }, }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts index 8b726d06f4602..cb1d0dc143efc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts @@ -123,17 +123,20 @@ function extractColumns( if (nodeOperation.input === 'fullReference') { const [referencedOp] = functions; const consumedParam = parseNode(referencedOp); + const hasActualMathContent = typeof consumedParam !== 'string'; - const subNodeVariables = consumedParam ? findVariables(consumedParam) : []; - const mathColumn = mathOperation.buildColumn({ - layer, - indexPattern, - }); - mathColumn.references = subNodeVariables.map(({ value }) => value); - mathColumn.params.tinymathAst = consumedParam!; - columns.push({ column: mathColumn }); - mathColumn.customLabel = true; - mathColumn.label = label; + if (hasActualMathContent) { + const subNodeVariables = consumedParam ? findVariables(consumedParam) : []; + const mathColumn = mathOperation.buildColumn({ + layer, + indexPattern, + }); + mathColumn.references = subNodeVariables.map(({ value }) => value); + mathColumn.params.tinymathAst = consumedParam!; + columns.push({ column: mathColumn }); + mathColumn.customLabel = true; + mathColumn.label = label; + } const mappedParams = getOperationParams(nodeOperation, namedArguments || []); const newCol = (nodeOperation as OperationDefinition< @@ -143,7 +146,11 @@ function extractColumns( { layer, indexPattern, - referenceIds: [getManagedId(idPrefix, columns.length - 1)], + referenceIds: [ + hasActualMathContent + ? getManagedId(idPrefix, columns.length - 1) + : (consumedParam as string), + ], }, mappedParams ); @@ -160,16 +167,19 @@ function extractColumns( if (root === undefined) { return []; } - const variables = findVariables(root); - const mathColumn = mathOperation.buildColumn({ - layer, - indexPattern, - }); - mathColumn.references = variables.map(({ value }) => value); - mathColumn.params.tinymathAst = root!; - mathColumn.customLabel = true; - mathColumn.label = label; - columns.push({ column: mathColumn }); + const topLevelMath = typeof root !== 'string'; + if (topLevelMath) { + const variables = findVariables(root); + const mathColumn = mathOperation.buildColumn({ + layer, + indexPattern, + }); + mathColumn.references = variables.map(({ value }) => value); + mathColumn.params.tinymathAst = root!; + mathColumn.customLabel = true; + mathColumn.label = label; + columns.push({ column: mathColumn }); + } return columns; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index 387a61ff79264..7de1318cbac61 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -25,6 +25,7 @@ import { documentField } from '../document_field'; import { getFieldByNameFactory } from '../pure_helpers'; import { generateId } from '../../id_generator'; import { createMockedFullReference, createMockedManagedReference } from './mocks'; +import { TinymathAST } from 'packages/kbn-tinymath'; jest.mock('../operations'); jest.mock('../../id_generator'); @@ -105,28 +106,34 @@ describe('state_helpers', () => { const source = { dataType: 'number' as const, isBucketed: false, - label: 'moving_average(sum(bytes), window=5)', + label: '5 + moving_average(sum(bytes), window=5)', operationType: 'formula' as const, params: { - formula: 'moving_average(sum(bytes), window=5)', + formula: '5 + moving_average(sum(bytes), window=5)', isFormulaBroken: false, }, - references: ['formulaX1'], + references: ['formulaX2'], }; const math = { customLabel: true, dataType: 'number' as const, isBucketed: false, - label: 'Part of moving_average(sum(bytes), window=5)', operationType: 'math' as const, - params: { tinymathAst: 'formulaX2' }, - references: ['formulaX2'], + label: 'Part of 5 + moving_average(sum(bytes), window=5)', + references: ['formulaX1'], + params: { + tinymathAst: { + type: 'function', + name: 'add', + args: [5, 'formulaX1'], + } as TinymathAST, + }, }; const sum = { customLabel: true, dataType: 'number' as const, isBucketed: false, - label: 'Part of moving_average(sum(bytes), window=5)', + label: 'Part of 5 + moving_average(sum(bytes), window=5)', operationType: 'sum' as const, scale: 'ratio' as const, sourceField: 'bytes', @@ -135,7 +142,7 @@ describe('state_helpers', () => { customLabel: true, dataType: 'number' as const, isBucketed: false, - label: 'Part of moving_average(sum(bytes), window=5)', + label: 'Part of 5 + moving_average(sum(bytes), window=5)', operationType: 'moving_average' as const, params: { window: 5 }, references: ['formulaX0'], @@ -148,14 +155,8 @@ describe('state_helpers', () => { columns: { source, formulaX0: sum, - formulaX1: math, - formulaX2: movingAvg, - formulaX3: { - ...math, - label: 'Part of moving_average(sum(bytes), window=5)', - references: ['formulaX2'], - params: { tinymathAst: 'formulaX2' }, - }, + formulaX1: movingAvg, + formulaX2: math, }, }, targetId: 'copy', @@ -171,40 +172,34 @@ describe('state_helpers', () => { 'formulaX0', 'formulaX1', 'formulaX2', - 'formulaX3', 'copyX0', 'copyX1', 'copyX2', - 'copyX3', 'copy', ], columns: { source, formulaX0: sum, - formulaX1: math, - formulaX2: movingAvg, - formulaX3: { - ...math, - references: ['formulaX2'], - params: { tinymathAst: 'formulaX2' }, - }, - copy: expect.objectContaining({ ...source, references: ['copyX3'] }), + formulaX1: movingAvg, + formulaX2: math, + copy: expect.objectContaining({ ...source, references: ['copyX2'] }), copyX0: expect.objectContaining({ ...sum, }), copyX1: expect.objectContaining({ - ...math, + ...movingAvg, references: ['copyX0'], - params: { tinymathAst: 'copyX0' }, }), copyX2: expect.objectContaining({ - ...movingAvg, - references: ['copyX1'], - }), - copyX3: expect.objectContaining({ ...math, - references: ['copyX2'], - params: { tinymathAst: 'copyX2' }, + references: ['copyX1'], + params: { + tinymathAst: expect.objectContaining({ + type: 'function', + name: 'add', + args: [5, 'copyX1'], + } as TinymathAST), + }, }), }, }); From 77d269fcc33633cc9492daef0a9709eea36946f9 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 10:06:10 -0400 Subject: [PATCH 10/43] Update chart reference docs (#102430) (#103093) * Update chart reference docs * Update from feedback * Update from review feedback * Update more from comments * Apply left alignment Co-authored-by: Wylie Conlon --- .../dashboard/aggregation-reference.asciidoc | 448 +++++++++++------- docs/user/dashboard/lens.asciidoc | 36 ++ 2 files changed, 309 insertions(+), 175 deletions(-) diff --git a/docs/user/dashboard/aggregation-reference.asciidoc b/docs/user/dashboard/aggregation-reference.asciidoc index cb5c484def3b9..17bfc19c2e0c9 100644 --- a/docs/user/dashboard/aggregation-reference.asciidoc +++ b/docs/user/dashboard/aggregation-reference.asciidoc @@ -12,91 +12,168 @@ This reference can help simplify the comparison if you need a specific feature. [options="header"] |=== -| Type | Aggregation-based | Lens | TSVB | Timelion | Vega +| Type | Lens | TSVB | Agg-based | Vega | Timelion | Table -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | | -| Table with summary row -^| X -^| X -| +| Bar, line, and area +| ✓ +| ✓ +| ✓ +| ✓ +| ✓ + +| Split chart/small multiples | +| ✓ +| ✓ +| ✓ | -| Bar, line, and area charts -^| X -^| X -^| X -^| X -^| X +| Pie and donut +| ✓ +| +| ✓ +| ✓ +| -| Percentage bar or area chart +| Sunburst +| ✓ | -^| X -^| X +| ✓ +| ✓ | -^| X -| Split bar, line, and area charts -^| X +| Treemap +| ✓ +| | +| ✓ | + +| Heat map +| ✓ +| ✓ +| ✓ +| ✓ | -^| X -| Pie and donut charts -^| X -^| X +| Gauge and Goal | +| ✓ +| ✓ +| ✓ | -^| X -| Sunburst chart -^| X -^| X +| Markdown +| +| ✓ | | | -| Heat map -^| X -^| X +| Metric +| ✓ +| ✓ +| ✓ +| ✓ +| + +| Tag cloud | | -^| X +| ✓ +| ✓ +| -| Gauge and Goal -^| X +|=== + +[float] +[[table-features]] +=== Table features + +[options="header"] +|=== + +| Type | Lens | TSVB | Agg-based + +| Summary row +| ✓ | -^| X +| ✓ + +| Pivot table +| ✓ | | -| Markdown +| Calculated column +| Formula +| ✓ +| Percent only + +| Color by value +| ✓ +| ✓ | + +|=== + +[float] +[[xy-features]] +=== Bar, line, area features + +[options="header"] +|=== + +| Type | Lens | TSVB | Agg-based | Vega | Timelion + +| Dense time series +| Customizable +| ✓ +| Customizable +| ✓ +| ✓ + +| Percentage mode +| ✓ +| ✓ +| ✓ +| ✓ | -^| X + +| Break downs +| 1 +| 1 +| 3 +| ∞ +| 1 + +| Custom color with break downs | +| Only for Filters +| ✓ +| ✓ | -| Metric -^| X -^| X -^| X +| Fit missing values +| ✓ | -^| X +| ✓ +| ✓ +| ✓ -| Tag cloud -^| X +| Synchronized tooltips +| +| ✓ | | | -^| X |=== @@ -111,67 +188,57 @@ For information about {es} bucket aggregations, refer to {ref}/search-aggregatio [options="header"] |=== -| Type | Agg-based | Markdown | Lens | TSVB +| Type | Lens | TSVB | Agg-based | Histogram -^| X -^| X -^| X +| ✓ | +| ✓ | Date histogram -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | Date range -^| X -^| X -| +| Use filters | +| ✓ | Filter -^| X -^| X | -^| X +| ✓ +| | Filters -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | GeoHash grid -^| X -^| X | | +| ✓ | IP range -^| X -^| X -| -| +| Use filters +| Use filters +| ✓ | Range -^| X -^| X -^| X -| +| ✓ +| Use filters +| ✓ | Terms -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | Significant terms -^| X -^| X | -^| X +| +| ✓ |=== @@ -186,67 +253,57 @@ For information about {es} metrics aggregations, refer to {ref}/search-aggregati [options="header"] |=== -| Type | Agg-based | Markdown | Lens | TSVB +| Type | Lens | TSVB | Agg-based | Metrics with filters +| ✓ | | -^| X -| - -| Average -^| X -^| X -^| X -^| X -| Sum -^| X -^| X -^| X -^| X +| Average, Sum, Max, Min +| ✓ +| ✓ +| ✓ | Unique count (Cardinality) -^| X -^| X -^| X -^| X - -| Max -^| X -^| X -^| X -^| X - -| Min -^| X -^| X -^| X -^| X - -| Percentiles -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ + +| Percentiles and Median +| ✓ +| ✓ +| ✓ | Percentiles Rank -^| X -^| X -| -^| X +| +| ✓ +| ✓ + +| Standard deviation +| +| ✓ +| ✓ + +| Sum of squares +| +| ✓ +| | Top hit (Last value) -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | Value count | | +| ✓ + +| Variance +| +| ✓ | -^| X |=== @@ -261,61 +318,94 @@ For information about {es} pipeline aggregations, refer to {ref}/search-aggregat [options="header"] |=== -| Type | Agg-based | Markdown | Lens | TSVB +| Type | Lens | TSVB | Agg-based | Avg bucket -^| X -^| X -| -^| X +| <> +| ✓ +| ✓ | Derivative -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | Max bucket -^| X -^| X -| -^| X +| <> +| ✓ +| ✓ | Min bucket -^| X -^| X -| -^| X +| <> +| ✓ +| ✓ | Sum bucket -^| X -^| X -| -^| X +| <> +| ✓ +| ✓ | Moving average -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | Cumulative sum -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | Bucket script | | +| ✓ + +| Bucket selector +| | -^| X +| | Serial differencing -^| X -^| X | -^| X +| ✓ +| ✓ + +|=== + +[float] +[[custom-functions]] +=== Additional functions + +[options="header"] +|=== + +| Type | Lens | TSVB | Agg-based + +| Counter rate +| ✓ +| ✓ +| + +| <> +| Use <> +| ✓ +| + +| <> +| +| ✓ +| + +| <> +| +| ✓ +| + +| Static value +| +| ✓ +| + |=== @@ -329,41 +419,49 @@ build their advanced visualization. [options="header"] |=== -| Type | Agg-based | Lens | TSVB | Timelion | Vega +| Type | Lens | TSVB | Agg-based | Vega | Timelion -| Math on aggregated data +| Math +| ✓ +| ✓ | -^| X -^| X -^| X -^| X +| ✓ +| ✓ | Visualize two indices +| ✓ +| ✓ | -^| X -^| X -^| X -^| X +| ✓ +| ✓ | Math across indices | | | -^| X -^| X +| ✓ +| ✓ | Time shifts +| ✓ +| ✓ | -^| X -^| X -^| X -^| X +| ✓ +| ✓ | Fully custom {es} queries | | | +| ✓ | -^| X + +| Normalize by time +| ✓ +| ✓ +| +| +| + |=== diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 4ecfcc9250122..2071f17ecff3d 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -139,6 +139,42 @@ image::images/lens_drag_drop_3.gif[Using drag and drop to reorder] . Press Space bar to confirm, or to cancel, press Esc. +[float] +[[lens-formulas]] +==== Use formulas to perform math + +Formulas let you perform math on aggregated data in Lens by typing +math and quick functions. To access formulas, +click the *Formula* tab in the dimension editor. Access the complete +reference for formulas from the help menu. + +The most common formulas are dividing two values to produce a percent. +To display accurately, set *Value format* to *Percent*. + +Filter ratio:: + +Use `kql=''` to filter one set of documents and compare it to other documents within the same grouping. +For example, to see how the error rate changes over time: ++ +``` +count(kql='response.status_code > 400') / count() +``` + +Week over week:: Use `shift='1w'` to get the value of each grouping from +the previous week. Time shift should not be used with the *Top values* function. ++ +``` +percentile(system.network.in.bytes, percentile=99) / +percentile(system.network.in.bytes, percentile=99, shift='1w') +``` + +Percent of total:: Formulas can calculate `overall_sum` for all the groupings, +which lets you convert each grouping into a percent of total: ++ +``` +sum(products.base_price) / overall_sum(sum(products.base_price)) +``` + [float] [[lens-faq]] ==== Frequently asked questions From 6e10c4bfc1ba5783baff81b63fc8fedfe1239d7c Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Wed, 23 Jun 2021 16:06:29 +0200 Subject: [PATCH 11/43] Cypress baseline for osquery (#102265) (#103077) * Cypress baseline for osquery * fix types * Update visual_config.ts Co-authored-by: Patryk Kopycinski Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Patryk Kopycinski Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/dev/typescript/projects.ts | 3 + x-pack/plugins/osquery/cypress/README.md | 138 ++++++++++++++++++ x-pack/plugins/osquery/cypress/cypress.json | 14 ++ .../integration/osquery_manager.spec.ts | 29 ++++ .../plugins/osquery/cypress/plugins/index.js | 29 ++++ .../osquery/cypress/screens/integrations.ts | 10 ++ .../osquery/cypress/screens/navigation.ts | 9 ++ .../osquery/cypress/screens/osquery.ts | 8 + .../osquery/cypress/support/commands.js | 32 ++++ .../plugins/osquery/cypress/support/index.ts | 30 ++++ .../osquery/cypress/tasks/integrations.ts | 20 +++ .../osquery/cypress/tasks/navigation.ts | 19 +++ x-pack/plugins/osquery/cypress/tsconfig.json | 15 ++ x-pack/plugins/osquery/package.json | 13 ++ x-pack/test/osquery_cypress/cli_config.ts | 19 +++ x-pack/test/osquery_cypress/config.ts | 43 ++++++ .../osquery_cypress/ftr_provider_context.d.ts | 12 ++ x-pack/test/osquery_cypress/runner.ts | 81 ++++++++++ x-pack/test/osquery_cypress/services.ts | 8 + x-pack/test/osquery_cypress/visual_config.ts | 19 +++ 20 files changed, 551 insertions(+) create mode 100644 x-pack/plugins/osquery/cypress/README.md create mode 100644 x-pack/plugins/osquery/cypress/cypress.json create mode 100644 x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.ts create mode 100644 x-pack/plugins/osquery/cypress/plugins/index.js create mode 100644 x-pack/plugins/osquery/cypress/screens/integrations.ts create mode 100644 x-pack/plugins/osquery/cypress/screens/navigation.ts create mode 100644 x-pack/plugins/osquery/cypress/screens/osquery.ts create mode 100644 x-pack/plugins/osquery/cypress/support/commands.js create mode 100644 x-pack/plugins/osquery/cypress/support/index.ts create mode 100644 x-pack/plugins/osquery/cypress/tasks/integrations.ts create mode 100644 x-pack/plugins/osquery/cypress/tasks/navigation.ts create mode 100644 x-pack/plugins/osquery/cypress/tsconfig.json create mode 100644 x-pack/plugins/osquery/package.json create mode 100644 x-pack/test/osquery_cypress/cli_config.ts create mode 100644 x-pack/test/osquery_cypress/config.ts create mode 100644 x-pack/test/osquery_cypress/ftr_provider_context.d.ts create mode 100644 x-pack/test/osquery_cypress/runner.ts create mode 100644 x-pack/test/osquery_cypress/services.ts create mode 100644 x-pack/test/osquery_cypress/visual_config.ts diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index 050743114f657..f372cf052d368 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -22,6 +22,9 @@ export const PROJECTS = [ new Project(resolve(REPO_ROOT, 'x-pack/plugins/security_solution/cypress/tsconfig.json'), { name: 'security_solution/cypress', }), + new Project(resolve(REPO_ROOT, 'x-pack/plugins/osquery/cypress/tsconfig.json'), { + name: 'osquery/cypress', + }), new Project(resolve(REPO_ROOT, 'x-pack/plugins/apm/e2e/tsconfig.json'), { name: 'apm/cypress', disableTypeCheck: true, diff --git a/x-pack/plugins/osquery/cypress/README.md b/x-pack/plugins/osquery/cypress/README.md new file mode 100644 index 0000000000000..0df311ebc0a05 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/README.md @@ -0,0 +1,138 @@ +# Cypress Tests + +The `osquery/cypress` directory contains functional UI tests that execute using [Cypress](https://www.cypress.io/). + +## Running the tests + +There are currently three ways to run the tests, comprised of two execution modes and two target environments, which will be detailed below. + +### Execution modes + +#### Interactive mode + +When you run Cypress in interactive mode, an interactive runner is displayed that allows you to see commands as they execute while also viewing the application under test. For more information, please see [cypress documentation](https://docs.cypress.io/guides/core-concepts/test-runner.html#Overview). + +#### Headless mode + +A headless browser is a browser simulation program that does not have a user interface. These programs operate like any other browser, but do not display any UI. This is why meanwhile you are executing the tests on this mode you are not going to see the application under test. Just the output of the test is displayed on the terminal once the execution is finished. + +### Target environments + +#### FTR (CI) + +This is the configuration used by CI. It uses the FTR to spawn both a Kibana instance (http://localhost:5620) and an Elasticsearch instance (http://localhost:9220) with a preloaded minimum set of data (see preceding "Test data" section), and then executes cypress against this stack. You can find this configuration in `x-pack/test/security_solution_cypress` + +### Test Execution: Examples + +#### FTR + Headless (Chrome) + +Since this is how tests are run on CI, this will likely be the configuration you want to reproduce failures locally, etc. + +```shell +# bootstrap kibana from the project root +yarn kbn bootstrap + +# build the plugins/assets that cypress will execute against +node scripts/build_kibana_platform_plugins + +# launch the cypress test runner +cd x-pack/plugins/security_solution +yarn cypress:run-as-ci +``` +#### FTR + Interactive + +This is the preferred mode for developing new tests. + +```shell +# bootstrap kibana from the project root +yarn kbn bootstrap + +# build the plugins/assets that cypress will execute against +node scripts/build_kibana_platform_plugins + +# launch the cypress test runner +cd x-pack/plugins/security_solution +yarn cypress:open-as-ci +``` + +Note that you can select the browser you want to use on the top right side of the interactive runner. + +## Folder Structure + +### integration/ + +Cypress convention. Contains the specs that are going to be executed. + +### fixtures/ + +Cypress convention. Fixtures are used as external pieces of static data when we stub responses. + +### plugins/ + +Cypress convention. As a convenience, by default Cypress will automatically include the plugins file cypress/plugins/index.js before every single spec file it runs. + +### screens/ + +Contains the elements we want to interact with in our tests. + +Each file inside the screens folder represents a screen in our application. + +### tasks/ + +_Tasks_ are functions that may be reused across tests. + +Each file inside the tasks folder represents a screen of our application. + +## Test data + +The data the tests need: + +- Is generated on the fly using our application APIs (preferred way) +- Is ingested on the ELS instance using the `es_archive` utility + +### How to generate a new archive + +**Note:** As mentioned above, archives are only meant to contain external data, e.g. beats data. Due to the tendency for archived domain objects (rules, signals) to quickly become out of date, it is strongly suggested that you generate this data within the test, through interaction with either the UI or the API. + +We use es_archiver to manage the data that our Cypress tests need. + +1. Set up a clean instance of kibana and elasticsearch (if this is not possible, try to clean/minimize the data that you are going to archive). +2. With the kibana and elasticsearch instance up and running, create the data that you need for your test. +3. When you are sure that you have all the data you need run the following command from: `x-pack/plugins/security_solution` + +```sh +node ../../../scripts/es_archiver save --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http://:@: +``` + +Example: + +```sh +node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http://elastic:changeme@localhost:9220 +``` + +Note that the command will create the folder if it does not exist. + +## Development Best Practices + +### Clean up the state + +Remember to clean up the state of the test after its execution, typically with the `cleanKibana` function. Be mindful of failure scenarios, as well: if your test fails, will it leave the environment in a recoverable state? + +### Minimize the use of es_archive + +When possible, create all the data that you need for executing the tests using the application APIS or the UI. + +### Speed up test execution time + +Loading the web page takes a big amount of time, in order to minimize that impact, the following points should be +taken into consideration until another solution is implemented: + +- Group the tests that are similar in different contexts. +- For every context login only once, clean the state between tests if needed without re-loading the page. +- All tests in a spec file must be order-independent. + +Remember that minimizing the number of times the web page is loaded, we minimize as well the execution time. + +## Linting + +Optional linting rules for Cypress and linting setup can be found [here](https://github.com/cypress-io/eslint-plugin-cypress#usage) diff --git a/x-pack/plugins/osquery/cypress/cypress.json b/x-pack/plugins/osquery/cypress/cypress.json new file mode 100644 index 0000000000000..eb24616607ec3 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/cypress.json @@ -0,0 +1,14 @@ +{ + "baseUrl": "http://localhost:5620", + "defaultCommandTimeout": 60000, + "execTimeout": 120000, + "pageLoadTimeout": 120000, + "nodeVersion": "system", + "retries": { + "runMode": 2 + }, + "trashAssetsBeforeRuns": false, + "video": false, + "viewportHeight": 900, + "viewportWidth": 1440 +} \ No newline at end of file diff --git a/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.ts b/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.ts new file mode 100644 index 0000000000000..0babfd2f10a8e --- /dev/null +++ b/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.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 { HEADER } from '../screens/osquery'; +import { OSQUERY_NAVIGATION_LINK } from '../screens/navigation'; + +import { INTEGRATIONS, OSQUERY, openNavigationFlyout, navigateTo } from '../tasks/navigation'; +import { addIntegration } from '../tasks/integrations'; + +describe('Osquery Manager', () => { + before(() => { + navigateTo(INTEGRATIONS); + addIntegration('Osquery Manager'); + }); + + it('Displays Osquery on the navigation flyout once installed ', () => { + openNavigationFlyout(); + cy.get(OSQUERY_NAVIGATION_LINK).should('exist'); + }); + + it('Displays Live queries history title when navigating to Osquery', () => { + navigateTo(OSQUERY); + cy.get(HEADER).should('have.text', 'Live queries history'); + }); +}); diff --git a/x-pack/plugins/osquery/cypress/plugins/index.js b/x-pack/plugins/osquery/cypress/plugins/index.js new file mode 100644 index 0000000000000..7dbb69ced7016 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/plugins/index.js @@ -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. + */ + +/// +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +/** + * @type {Cypress.PluginConfig} + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +module.exports = (_on, _config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +}; diff --git a/x-pack/plugins/osquery/cypress/screens/integrations.ts b/x-pack/plugins/osquery/cypress/screens/integrations.ts new file mode 100644 index 0000000000000..0b29e857f46ee --- /dev/null +++ b/x-pack/plugins/osquery/cypress/screens/integrations.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 const ADD_POLICY_BTN = '[data-test-subj="addIntegrationPolicyButton"]'; +export const CREATE_PACKAGE_POLICY_SAVE_BTN = '[data-test-subj="createPackagePolicySaveButton"]'; +export const INTEGRATIONS_CARD = '.euiCard__titleAnchor'; diff --git a/x-pack/plugins/osquery/cypress/screens/navigation.ts b/x-pack/plugins/osquery/cypress/screens/navigation.ts new file mode 100644 index 0000000000000..7884cf347d7c0 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/screens/navigation.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const TOGGLE_NAVIGATION_BTN = '[data-test-subj="toggleNavButton"]'; +export const OSQUERY_NAVIGATION_LINK = '[data-test-subj="collapsibleNavAppLink"] [title="Osquery"]'; diff --git a/x-pack/plugins/osquery/cypress/screens/osquery.ts b/x-pack/plugins/osquery/cypress/screens/osquery.ts new file mode 100644 index 0000000000000..bc387a57e9e3c --- /dev/null +++ b/x-pack/plugins/osquery/cypress/screens/osquery.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const HEADER = 'h1'; diff --git a/x-pack/plugins/osquery/cypress/support/commands.js b/x-pack/plugins/osquery/cypress/support/commands.js new file mode 100644 index 0000000000000..66f9435035571 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/support/commands.js @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) diff --git a/x-pack/plugins/osquery/cypress/support/index.ts b/x-pack/plugins/osquery/cypress/support/index.ts new file mode 100644 index 0000000000000..72618c943f4d2 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/support/index.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. + */ + +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') +Cypress.on('uncaught:exception', () => { + return false; +}); diff --git a/x-pack/plugins/osquery/cypress/tasks/integrations.ts b/x-pack/plugins/osquery/cypress/tasks/integrations.ts new file mode 100644 index 0000000000000..f85ef56550af5 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/tasks/integrations.ts @@ -0,0 +1,20 @@ +/* + * Copyright 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 { + ADD_POLICY_BTN, + CREATE_PACKAGE_POLICY_SAVE_BTN, + INTEGRATIONS_CARD, +} from '../screens/integrations'; + +export const addIntegration = (integration: string) => { + cy.get(INTEGRATIONS_CARD).contains(integration).click(); + cy.get(ADD_POLICY_BTN).click(); + cy.get(CREATE_PACKAGE_POLICY_SAVE_BTN).click(); + cy.get(CREATE_PACKAGE_POLICY_SAVE_BTN).should('not.exist'); + cy.reload(); +}; diff --git a/x-pack/plugins/osquery/cypress/tasks/navigation.ts b/x-pack/plugins/osquery/cypress/tasks/navigation.ts new file mode 100644 index 0000000000000..63d6b205b433b --- /dev/null +++ b/x-pack/plugins/osquery/cypress/tasks/navigation.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 { TOGGLE_NAVIGATION_BTN } from '../screens/navigation'; + +export const INTEGRATIONS = 'app/integrations#/'; +export const OSQUERY = 'app/osquery/live_queries'; + +export const navigateTo = (page: string) => { + cy.visit(page); +}; + +export const openNavigationFlyout = () => { + cy.get(TOGGLE_NAVIGATION_BTN).click(); +}; diff --git a/x-pack/plugins/osquery/cypress/tsconfig.json b/x-pack/plugins/osquery/cypress/tsconfig.json new file mode 100644 index 0000000000000..467ea13fc4869 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../tsconfig.base.json", + "exclude": [], + "include": [ + "./**/*" + ], + "compilerOptions": { + "tsBuildInfoFile": "../../../../build/tsbuildinfo/osquery/cypress", + "types": [ + "cypress", + "node" + ], + "resolveJsonModule": true, + }, + } diff --git a/x-pack/plugins/osquery/package.json b/x-pack/plugins/osquery/package.json new file mode 100644 index 0000000000000..5bbb95e556d6b --- /dev/null +++ b/x-pack/plugins/osquery/package.json @@ -0,0 +1,13 @@ +{ + "author": "Elastic", + "name": "osquery", + "version": "8.0.0", + "private": true, + "license": "Elastic-License", + "scripts": { + "cypress:open": "../../../node_modules/.bin/cypress open --config-file ./cypress/cypress.json", + "cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/osquery_cypress/visual_config.ts", + "cypress:run": "../../../node_modules/.bin/cypress run --config-file ./cypress/cypress.json", + "cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/osquery_cypress/cli_config.ts" + } +} diff --git a/x-pack/test/osquery_cypress/cli_config.ts b/x-pack/test/osquery_cypress/cli_config.ts new file mode 100644 index 0000000000000..d0de73151952d --- /dev/null +++ b/x-pack/test/osquery_cypress/cli_config.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 { FtrConfigProviderContext } from '@kbn/test'; + +import { OsqueryCypressCliTestRunner } from './runner'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const osqueryCypressConfig = await readConfigFile(require.resolve('./config.ts')); + return { + ...osqueryCypressConfig.getAll(), + + testRunner: OsqueryCypressCliTestRunner, + }; +} diff --git a/x-pack/test/osquery_cypress/config.ts b/x-pack/test/osquery_cypress/config.ts new file mode 100644 index 0000000000000..18b4605fb9d8b --- /dev/null +++ b/x-pack/test/osquery_cypress/config.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +import { CA_CERT_PATH } from '@kbn/dev-utils'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const kibanaCommonTestsConfig = await readConfigFile( + require.resolve('../../../test/common/config.js') + ); + const xpackFunctionalTestsConfig = await readConfigFile( + require.resolve('../functional/config.js') + ); + + return { + ...kibanaCommonTestsConfig.getAll(), + + esTestCluster: { + ...xpackFunctionalTestsConfig.get('esTestCluster'), + serverArgs: [ + ...xpackFunctionalTestsConfig.get('esTestCluster.serverArgs'), + // define custom es server here + // API Keys is enabled at the top level + 'xpack.security.enabled=true', + ], + }, + + kbnTestServer: { + ...xpackFunctionalTestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xpackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), + '--csp.strict=false', + // define custom kibana server args here + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + ], + }, + }; +} diff --git a/x-pack/test/osquery_cypress/ftr_provider_context.d.ts b/x-pack/test/osquery_cypress/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..aa56557c09df8 --- /dev/null +++ b/x-pack/test/osquery_cypress/ftr_provider_context.d.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GenericFtrProviderContext } from '@kbn/test'; + +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/osquery_cypress/runner.ts b/x-pack/test/osquery_cypress/runner.ts new file mode 100644 index 0000000000000..32c84af5faf76 --- /dev/null +++ b/x-pack/test/osquery_cypress/runner.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { resolve } from 'path'; +import Url from 'url'; + +import { withProcRunner } from '@kbn/dev-utils'; + +import { FtrProviderContext } from './ftr_provider_context'; + +export async function OsqueryCypressCliTestRunner({ getService }: FtrProviderContext) { + const log = getService('log'); + const config = getService('config'); + + await withProcRunner(log, async (procs) => { + await procs.run('cypress', { + cmd: 'yarn', + args: ['cypress:run'], + cwd: resolve(__dirname, '../../plugins/osquery'), + env: { + FORCE_COLOR: '1', + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_baseUrl: Url.format(config.get('servers.kibana')), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_protocol: config.get('servers.kibana.protocol'), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_hostname: config.get('servers.kibana.hostname'), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_configport: config.get('servers.kibana.port'), + CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')), + CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'), + CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'), + CYPRESS_KIBANA_URL: Url.format({ + protocol: config.get('servers.kibana.protocol'), + hostname: config.get('servers.kibana.hostname'), + port: config.get('servers.kibana.port'), + }), + ...process.env, + }, + wait: true, + }); + }); +} + +export async function OsqueryCypressVisualTestRunner({ getService }: FtrProviderContext) { + const log = getService('log'); + const config = getService('config'); + + await withProcRunner(log, async (procs) => { + await procs.run('cypress', { + cmd: 'yarn', + args: ['cypress:open'], + cwd: resolve(__dirname, '../../plugins/osquery'), + env: { + FORCE_COLOR: '1', + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_baseUrl: Url.format(config.get('servers.kibana')), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_protocol: config.get('servers.kibana.protocol'), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_hostname: config.get('servers.kibana.hostname'), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_configport: config.get('servers.kibana.port'), + CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')), + CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'), + CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'), + CYPRESS_KIBANA_URL: Url.format({ + protocol: config.get('servers.kibana.protocol'), + hostname: config.get('servers.kibana.hostname'), + port: config.get('servers.kibana.port'), + }), + ...process.env, + }, + wait: true, + }); + }); +} diff --git a/x-pack/test/osquery_cypress/services.ts b/x-pack/test/osquery_cypress/services.ts new file mode 100644 index 0000000000000..5e063134081ad --- /dev/null +++ b/x-pack/test/osquery_cypress/services.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from '../../../test/common/services'; diff --git a/x-pack/test/osquery_cypress/visual_config.ts b/x-pack/test/osquery_cypress/visual_config.ts new file mode 100644 index 0000000000000..35ffe311fdc27 --- /dev/null +++ b/x-pack/test/osquery_cypress/visual_config.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 { FtrConfigProviderContext } from '@kbn/test'; + +import { OsqueryCypressVisualTestRunner } from './runner'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const osqueryCypressConfig = await readConfigFile(require.resolve('./config.ts')); + return { + ...osqueryCypressConfig.getAll(), + + testRunner: OsqueryCypressVisualTestRunner, + }; +} From 056cf014c0ec74281384df96d5c61b0b82664cd6 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 10:23:04 -0400 Subject: [PATCH 12/43] Allow restored session to run missing searches and show a warning (#101650) (#103078) * Allow restored session to run missing searches and show a warning * tests and docs * improve warning * tests for new functionality NoSearchIdInSessionError type * managmeent tests * Update texts * fix search service pus * link to docs * imports * format import Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Liza Katz --- ...-plugin-core-public.doclinksstart.links.md | 1 + ...kibana-plugin-core-public.doclinksstart.md | 2 +- ...public.ikibanasearchresponse.isrestored.md | 13 +++ ...ugins-data-public.ikibanasearchresponse.md | 1 + .../kibana-plugin-plugins-data-server.md | 1 + ....nosearchidinsessionerror._constructor_.md | 13 +++ ...ns-data-server.nosearchidinsessionerror.md | 18 ++++ .../public/doc_links/doc_links_service.ts | 2 + src/core/public/public.api.md | 1 + src/plugins/data/common/search/types.ts | 5 + src/plugins/data/public/public.api.md | 1 + .../data/public/search/errors/index.ts | 1 + .../search_session_incomplete_warning.tsx | 31 +++++++ .../search_interceptor.test.ts | 93 +++++++++++++++++++ .../search_interceptor/search_interceptor.ts | 27 ++++++ src/plugins/data/server/index.ts | 1 + .../search/errors/no_search_id_in_session.ts | 15 +++ src/plugins/data/server/search/index.ts | 1 + .../data/server/search/search_service.test.ts | 17 ++++ .../data/server/search/search_service.ts | 43 +++++++-- src/plugins/data/server/server.api.md | 32 ++++--- .../sessions_mgmt/components/status.test.tsx | 1 + .../components/table/table.test.tsx | 6 +- .../search/sessions_mgmt/lib/api.test.ts | 3 + .../public/search/sessions_mgmt/lib/api.ts | 2 + .../sessions_mgmt/lib/get_columns.test.tsx | 36 ++++++- .../search/sessions_mgmt/lib/get_columns.tsx | 14 +++ .../public/search/sessions_mgmt/types.ts | 1 + .../server/search/session/session_service.ts | 8 +- .../api_integration/apis/search/session.ts | 7 +- 30 files changed, 366 insertions(+), 31 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror.md create mode 100644 src/plugins/data/public/search/errors/search_session_incomplete_warning.tsx create mode 100644 src/plugins/data/server/search/errors/no_search_id_in_session.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index ae433e3db14c6..b10ad949c4944 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -106,6 +106,7 @@ readonly links: { }; readonly search: { readonly sessions: string; + readonly sessionLimits: string; }; readonly indexPatterns: { readonly introduction: string; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index b0800c7dfc65e..c020f57faa882 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
} | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md new file mode 100644 index 0000000000000..d649212ae0547 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IKibanaSearchResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.md) > [isRestored](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md) + +## IKibanaSearchResponse.isRestored property + +Indicates whether the results returned are from the async-search index + +Signature: + +```typescript +isRestored?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md index 1d3e0c08dfc18..c7046902dac72 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md @@ -16,6 +16,7 @@ export interface IKibanaSearchResponse | --- | --- | --- | | [id](./kibana-plugin-plugins-data-public.ikibanasearchresponse.id.md) | string | Some responses may contain a unique id to identify the request this response came from. | | [isPartial](./kibana-plugin-plugins-data-public.ikibanasearchresponse.ispartial.md) | boolean | Indicates whether the results returned are complete or partial | +| [isRestored](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md) | boolean | Indicates whether the results returned are from the async-search index | | [isRunning](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrunning.md) | boolean | Indicates whether search is still in flight | | [loaded](./kibana-plugin-plugins-data-public.ikibanasearchresponse.loaded.md) | number | If relevant to the search strategy, return a loaded number that represents how progress is indicated. | | [rawResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.rawresponse.md) | RawResponse | The raw response returned by the internal search method (usually the raw ES response) | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index b1745b298e27e..9816b884c4614 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -13,6 +13,7 @@ | [IndexPatternsFetcher](./kibana-plugin-plugins-data-server.indexpatternsfetcher.md) | | | [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) | | | [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) | | +| [NoSearchIdInSessionError](./kibana-plugin-plugins-data-server.nosearchidinsessionerror.md) | | | [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) | | | [Plugin](./kibana-plugin-plugins-data-server.plugin.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md new file mode 100644 index 0000000000000..e48a1c98f8578 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [NoSearchIdInSessionError](./kibana-plugin-plugins-data-server.nosearchidinsessionerror.md) > [(constructor)](./kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md) + +## NoSearchIdInSessionError.(constructor) + +Constructs a new instance of the `NoSearchIdInSessionError` class + +Signature: + +```typescript +constructor(); +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror.md new file mode 100644 index 0000000000000..707739f845cd1 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [NoSearchIdInSessionError](./kibana-plugin-plugins-data-server.nosearchidinsessionerror.md) + +## NoSearchIdInSessionError class + +Signature: + +```typescript +export declare class NoSearchIdInSessionError extends KbnError +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)()](./kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md) | | Constructs a new instance of the NoSearchIdInSessionError class | + diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index c737f41cdabc5..f19ae2d9af3c2 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -205,6 +205,7 @@ export class DocLinksService { }, search: { sessions: `${KIBANA_DOCS}search-sessions.html`, + sessionLimits: `${KIBANA_DOCS}search-sessions.html#_limitations`, }, date: { dateMath: `${ELASTICSEARCH_DOCS}common-options.html#date-math`, @@ -525,6 +526,7 @@ export interface DocLinksStart { }; readonly search: { readonly sessions: string; + readonly sessionLimits: string; }; readonly indexPatterns: { readonly introduction: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 27569935bcc65..31e85341fb519 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -585,6 +585,7 @@ export interface DocLinksStart { }; readonly search: { readonly sessions: string; + readonly sessionLimits: string; }; readonly indexPatterns: { readonly introduction: string; diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts index d1890ec97df4e..c5cf3f9f09e6c 100644 --- a/src/plugins/data/common/search/types.ts +++ b/src/plugins/data/common/search/types.ts @@ -65,6 +65,11 @@ export interface IKibanaSearchResponse { */ isPartial?: boolean; + /** + * Indicates whether the results returned are from the async-search index + */ + isRestored?: boolean; + /** * The raw response returned by the internal search method (usually the raw ES response) */ diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 8e2f28d75166a..9076b0a5770f1 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1353,6 +1353,7 @@ export interface IKibanaSearchRequest { export interface IKibanaSearchResponse { id?: string; isPartial?: boolean; + isRestored?: boolean; isRunning?: boolean; loaded?: number; rawResponse: RawResponse; diff --git a/src/plugins/data/public/search/errors/index.ts b/src/plugins/data/public/search/errors/index.ts index 82c9e04b79798..fcdea8dec1c2e 100644 --- a/src/plugins/data/public/search/errors/index.ts +++ b/src/plugins/data/public/search/errors/index.ts @@ -12,3 +12,4 @@ export * from './timeout_error'; export * from './utils'; export * from './types'; export * from './http_error'; +export * from './search_session_incomplete_warning'; diff --git a/src/plugins/data/public/search/errors/search_session_incomplete_warning.tsx b/src/plugins/data/public/search/errors/search_session_incomplete_warning.tsx new file mode 100644 index 0000000000000..c5c5c37f31cf8 --- /dev/null +++ b/src/plugins/data/public/search/errors/search_session_incomplete_warning.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { CoreStart } from 'kibana/public'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const SearchSessionIncompleteWarning = (docLinks: CoreStart['docLinks']) => ( + <> + + It needs more time to fully render. You can wait here or come back to it later. + + + + + + + +); diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts index fe66d4b6e9937..155638250a2a4 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts @@ -29,6 +29,12 @@ jest.mock('./utils', () => ({ }), })); +jest.mock('../errors/search_session_incomplete_warning', () => ({ + SearchSessionIncompleteWarning: jest.fn(), +})); + +import { SearchSessionIncompleteWarning } from '../errors/search_session_incomplete_warning'; + let searchInterceptor: SearchInterceptor; let mockCoreSetup: MockedKeys; let bfetchSetup: jest.Mocked; @@ -508,6 +514,7 @@ describe('SearchInterceptor', () => { } : null ); + sessionServiceMock.isRestore.mockReturnValue(!!opts?.isRestore); fetchMock.mockResolvedValue({ result: 200 }); }; @@ -562,6 +569,92 @@ describe('SearchInterceptor', () => { (sessionService as jest.Mocked).getSearchOptions ).toHaveBeenCalledWith(sessionId); }); + + test('should not show warning if a search is available during restore', async () => { + setup({ + isRestore: true, + isStored: true, + sessionId: '123', + }); + + const responses = [ + { + time: 10, + value: { + isPartial: false, + isRunning: false, + isRestored: true, + id: 1, + rawResponse: { + took: 1, + }, + }, + }, + ]; + mockFetchImplementation(responses); + + const response = searchInterceptor.search( + {}, + { + sessionId: '123', + } + ); + response.subscribe({ next, error, complete }); + + await timeTravel(10); + + expect(SearchSessionIncompleteWarning).toBeCalledTimes(0); + }); + + test('should show warning once if a search is not available during restore', async () => { + setup({ + isRestore: true, + isStored: true, + sessionId: '123', + }); + + const responses = [ + { + time: 10, + value: { + isPartial: false, + isRunning: false, + isRestored: false, + id: 1, + rawResponse: { + took: 1, + }, + }, + }, + ]; + mockFetchImplementation(responses); + + searchInterceptor + .search( + {}, + { + sessionId: '123', + } + ) + .subscribe({ next, error, complete }); + + await timeTravel(10); + + expect(SearchSessionIncompleteWarning).toBeCalledTimes(1); + + searchInterceptor + .search( + {}, + { + sessionId: '123', + } + ) + .subscribe({ next, error, complete }); + + await timeTravel(10); + + expect(SearchSessionIncompleteWarning).toBeCalledTimes(1); + }); }); describe('Session tracking', () => { diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts index 57b156a9b3c00..e0e1df65101c7 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts @@ -43,6 +43,7 @@ import { PainlessError, SearchTimeoutError, TimeoutErrorMode, + SearchSessionIncompleteWarning, } from '../errors'; import { toMountPoint } from '../../../../kibana_react/public'; import { AbortError, KibanaServerError } from '../../../../kibana_utils/public'; @@ -82,6 +83,7 @@ export class SearchInterceptor { * @internal */ private application!: CoreStart['application']; + private docLinks!: CoreStart['docLinks']; private batchedFetch!: BatchedFunc< { request: IKibanaSearchRequest; options: ISearchOptionsSerializable }, IKibanaSearchResponse @@ -95,6 +97,7 @@ export class SearchInterceptor { this.deps.startServices.then(([coreStart]) => { this.application = coreStart.application; + this.docLinks = coreStart.docLinks; }); this.batchedFetch = deps.bfetch.batchedFunction({ @@ -345,6 +348,11 @@ export class SearchInterceptor { this.handleSearchError(e, searchOptions, searchAbortController.isTimeout()) ); }), + tap((response) => { + if (this.deps.session.isRestore() && response.isRestored === false) { + this.showRestoreWarning(this.deps.session.getSessionId()); + } + }), finalize(() => { this.pendingCount$.next(this.pendingCount$.getValue() - 1); if (untrackSearch && this.deps.session.isCurrentSession(sessionId)) { @@ -371,6 +379,25 @@ export class SearchInterceptor { } ); + private showRestoreWarningToast = (sessionId?: string) => { + this.deps.toasts.addWarning( + { + title: 'Your search session is still running', + text: toMountPoint(SearchSessionIncompleteWarning(this.docLinks)), + }, + { + toastLifeTimeMs: 60000, + } + ); + }; + + private showRestoreWarning = memoize( + this.showRestoreWarningToast, + (_: SearchTimeoutError, sessionId: string) => { + return sessionId; + } + ); + /** * Show one error notification per session. * @internal diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 0764f4f441e42..dd60951e6d228 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -238,6 +238,7 @@ export { DataRequestHandlerContext, AsyncSearchResponse, AsyncSearchStatusResponse, + NoSearchIdInSessionError, } from './search'; // Search namespace diff --git a/src/plugins/data/server/search/errors/no_search_id_in_session.ts b/src/plugins/data/server/search/errors/no_search_id_in_session.ts new file mode 100644 index 0000000000000..b291df1cee5ba --- /dev/null +++ b/src/plugins/data/server/search/errors/no_search_id_in_session.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { KbnError } from '../../../../kibana_utils/common'; + +export class NoSearchIdInSessionError extends KbnError { + constructor() { + super('No search ID in this session matching the given search request'); + } +} diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts index 812f3171aef99..b9affe96ea2dd 100644 --- a/src/plugins/data/server/search/index.ts +++ b/src/plugins/data/server/search/index.ts @@ -13,3 +13,4 @@ export * from './strategies/eql_search'; export { usageProvider, SearchUsage, searchUsageObserver } from './collectors'; export * from './aggs'; export * from './session'; +export * from './errors/no_search_id_in_session'; diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts index 52ee8e60a5b26..314cb2c3acbf8 100644 --- a/src/plugins/data/server/search/search_service.test.ts +++ b/src/plugins/data/server/search/search_service.test.ts @@ -25,6 +25,7 @@ import { ISearchSessionService, ISearchStart, ISearchStrategy, + NoSearchIdInSessionError, } from '.'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { expressionsPluginMock } from '../../../expressions/public/mocks'; @@ -175,6 +176,22 @@ describe('Search service', () => { expect(request).toStrictEqual({ ...searchRequest, id: 'my_id' }); }); + it('searches even if id is not found in session during restore', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: true, isRestore: true }; + + mockSessionClient.getId = jest.fn().mockImplementation(() => { + throw new NoSearchIdInSessionError(); + }); + + const res = await mockScopedClient.search(searchRequest, options).toPromise(); + + const [request, callOptions] = mockStrategy.search.mock.calls[0]; + expect(callOptions).toBe(options); + expect(request).toStrictEqual({ ...searchRequest }); + expect(res.isRestored).toBe(false); + }); + it('does not fail if `trackId` throws', async () => { const searchRequest = { params: {} }; const options = { sessionId, isStored: false, isRestore: false }; diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index a651d7b3bf105..00dffefa5e3a6 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -19,7 +19,7 @@ import { SharedGlobalConfig, StartServicesAccessor, } from 'src/core/server'; -import { first, switchMap, tap } from 'rxjs/operators'; +import { first, map, switchMap, tap, withLatestFrom } from 'rxjs/operators'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import type { @@ -80,6 +80,7 @@ import { registerBsearchRoute } from './routes/bsearch'; import { getKibanaContext } from './expressions/kibana_context'; import { enhancedEsSearchStrategyProvider } from './strategies/ese_search'; import { eqlSearchStrategyProvider } from './strategies/eql_search'; +import { NoSearchIdInSessionError } from './errors/no_search_id_in_session'; type StrategyMap = Record>; @@ -287,24 +288,48 @@ export class SearchService implements Plugin { options.strategy ); - const getSearchRequest = async () => - !options.sessionId || !options.isRestore || request.id - ? request - : { + const getSearchRequest = async () => { + if (!options.sessionId || !options.isRestore || request.id) { + return request; + } else { + try { + const id = await deps.searchSessionsClient.getId(request, options); + this.logger.debug(`Found search session id for request ${id}`); + return { ...request, - id: await deps.searchSessionsClient.getId(request, options), + id, }; + } catch (e) { + if (e instanceof NoSearchIdInSessionError) { + this.logger.debug('Ignoring missing search ID'); + return request; + } else { + throw e; + } + } + } + }; - return from(getSearchRequest()).pipe( + const searchRequest$ = from(getSearchRequest()); + const search$ = searchRequest$.pipe( switchMap((searchRequest) => strategy.search(searchRequest, options, deps)), - tap((response) => { - if (!options.sessionId || !response.id || options.isRestore) return; + withLatestFrom(searchRequest$), + tap(([response, requestWithId]) => { + if (!options.sessionId || !response.id || (options.isRestore && requestWithId.id)) return; // intentionally swallow tracking error, as it shouldn't fail the search deps.searchSessionsClient.trackId(request, response.id, options).catch((trackErr) => { this.logger.error(trackErr); }); + }), + map(([response, requestWithId]) => { + return { + ...response, + isRestored: !!requestWithId.id, + }; }) ); + + return search$; } catch (e) { return throwError(e); } diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index f0466dd317ab7..44be1a2255433 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -1211,6 +1211,14 @@ export enum METRIC_TYPES { TOP_HITS = "top_hits" } +// Warning: (ae-forgotten-export) The symbol "KbnError" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "NoSearchIdInSessionError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class NoSearchIdInSessionError extends KbnError { + constructor(); +} + // Warning: (ae-missing-release-tag) "OptionedParamType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1543,18 +1551,18 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "HistogramFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:128:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:128:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:244:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:244:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:246:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:247:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:256:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:270:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:245:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:245:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:247:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:248:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:259:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:268:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:272:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:81:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // src/plugins/data/server/search/types.ts:115:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx index 86f5564a17d52..59da0f0f4d17e 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx @@ -27,6 +27,7 @@ describe('Background Search Session management status labels', () => { id: 'wtywp9u2802hahgp-gsla', restoreUrl: '/app/great-app-url/#45', reloadUrl: '/app/great-app-url/#45', + numSearches: 1, appId: 'security', status: SearchSessionStatus.IN_PROGRESS, created: '2020-12-02T00:19:32Z', diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx index 42ff270ed44a0..6dfe3a5153670 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx @@ -70,6 +70,7 @@ describe('Background Search Session Management Table', () => { status: SearchSessionStatus.IN_PROGRESS, created: '2020-12-02T00:19:32Z', expires: '2020-12-07T00:19:32Z', + idMapping: {}, }, }, ], @@ -95,10 +96,12 @@ describe('Background Search Session Management Table', () => { ); }); - expect(table.find('thead th').map((node) => node.text())).toMatchInlineSnapshot(` + expect(table.find('thead th .euiTableCellContent__text').map((node) => node.text())) + .toMatchInlineSnapshot(` Array [ "App", "Name", + "# Searches", "Status", "Created", "Expiration", @@ -130,6 +133,7 @@ describe('Background Search Session Management Table', () => { Array [ "App", "Namevery background search ", + "# Searches0", "StatusExpired", "Created2 Dec, 2020, 00:19:32", "Expiration--", diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts index 3857b08ad0a3a..cc79f8002a98c 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts @@ -52,6 +52,7 @@ describe('Search Sessions Management API', () => { status: 'complete', initialState: {}, restoreState: {}, + idMapping: [], }, }, ], @@ -78,6 +79,7 @@ describe('Search Sessions Management API', () => { "id": "hello-pizza-123", "initialState": Object {}, "name": "Veggie", + "numSearches": 0, "reloadUrl": "hello-cool-undefined-url", "restoreState": Object {}, "restoreUrl": "hello-cool-undefined-url", @@ -100,6 +102,7 @@ describe('Search Sessions Management API', () => { expires: moment().subtract(3, 'days'), initialState: {}, restoreState: {}, + idMapping: {}, }, }, ], diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts index 3710dfa16e76b..0369dc4a839b5 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts @@ -90,6 +90,7 @@ const mapToUISession = (urls: UrlGeneratorsStart, config: SessionsConfigSchema) urlGeneratorId, initialState, restoreState, + idMapping, } = savedObject.attributes; const status = getUIStatus(savedObject.attributes); @@ -113,6 +114,7 @@ const mapToUISession = (urls: UrlGeneratorsStart, config: SessionsConfigSchema) reloadUrl, initialState, restoreState, + numSearches: Object.keys(idMapping).length, }; }; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx index 4b68e0c9e2afd..fc4e67360ea4a 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx @@ -70,6 +70,7 @@ describe('Search Sessions Management table column factory', () => { reloadUrl: '/app/great-app-url', restoreUrl: '/app/great-app-url/#42', appId: 'discovery', + numSearches: 3, status: SearchSessionStatus.IN_PROGRESS, created: '2020-12-02T00:19:32Z', expires: '2020-12-07T00:19:32Z', @@ -95,6 +96,12 @@ describe('Search Sessions Management table column factory', () => { "sortable": true, "width": "20%", }, + Object { + "field": "numSearches", + "name": "# Searches", + "render": [Function], + "sortable": true, + }, Object { "field": "status", "name": "Status", @@ -146,10 +153,29 @@ describe('Search Sessions Management table column factory', () => { }); }); + // Num of searches column + describe('num of searches', () => { + test('renders', () => { + const [, , numOfSearches] = getColumns( + mockCoreStart, + mockPluginsSetup, + api, + mockConfig, + tz, + handleAction + ) as Array>; + + const numOfSearchesLine = mount( + numOfSearches.render!(mockSession.numSearches, mockSession) as ReactElement + ); + expect(numOfSearchesLine.text()).toMatchInlineSnapshot(`"3"`); + }); + }); + // Status column describe('status', () => { test('render in_progress', () => { - const [, , status] = getColumns( + const [, , , status] = getColumns( mockCoreStart, mockPluginsSetup, api, @@ -165,7 +191,7 @@ describe('Search Sessions Management table column factory', () => { }); test('error handling', () => { - const [, , status] = getColumns( + const [, , , status] = getColumns( mockCoreStart, mockPluginsSetup, api, @@ -188,7 +214,7 @@ describe('Search Sessions Management table column factory', () => { test('render using Browser timezone', () => { tz = 'Browser'; - const [, , , createdDateCol] = getColumns( + const [, , , , createdDateCol] = getColumns( mockCoreStart, mockPluginsSetup, api, @@ -205,7 +231,7 @@ describe('Search Sessions Management table column factory', () => { test('render using AK timezone', () => { tz = 'US/Alaska'; - const [, , , createdDateCol] = getColumns( + const [, , , , createdDateCol] = getColumns( mockCoreStart, mockPluginsSetup, api, @@ -220,7 +246,7 @@ describe('Search Sessions Management table column factory', () => { }); test('error handling', () => { - const [, , , createdDateCol] = getColumns( + const [, , , , createdDateCol] = getColumns( mockCoreStart, mockPluginsSetup, api, diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx index 1805ef52b85f1..d8d2fa0aeac59 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx @@ -120,6 +120,20 @@ export const getColumns = ( }, }, + // # Searches + { + field: 'numSearches', + name: i18n.translate('xpack.data.mgmt.searchSessions.table.numSearches', { + defaultMessage: '# Searches', + }), + sortable: true, + render: (numSearches: UISession['numSearches'], session) => ( + + {numSearches} + + ), + }, + // Session status { field: 'status', diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts index d0d5ee9fb17dd..6a8ace8dbdc79 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts @@ -34,6 +34,7 @@ export interface UISession { created: string; expires: string | null; status: UISearchSessionState; + numSearches: number; actions?: ACTION[]; reloadUrl: string; restoreUrl: string; diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index 138f42549a094..81a12f607935d 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -24,7 +24,11 @@ import { ENHANCED_ES_SEARCH_STRATEGY, SEARCH_SESSION_TYPE, } from '../../../../../../src/plugins/data/common'; -import { esKuery, ISearchSessionService } from '../../../../../../src/plugins/data/server'; +import { + esKuery, + ISearchSessionService, + NoSearchIdInSessionError, +} from '../../../../../../src/plugins/data/server'; import { AuthenticatedUser, SecurityPluginSetup } from '../../../../security/server'; import { TaskManagerSetupContract, @@ -436,7 +440,7 @@ export class SearchSessionService const requestHash = createRequestHash(searchRequest.params); if (!session.attributes.idMapping.hasOwnProperty(requestHash)) { this.logger.error(`getId | ${sessionId} | ${requestHash} not found`); - throw new Error('No search ID in this session matching the given search request'); + throw new NoSearchIdInSessionError(); } this.logger.debug(`getId | ${sessionId} | ${requestHash}`); diff --git a/x-pack/test/api_integration/apis/search/session.ts b/x-pack/test/api_integration/apis/search/session.ts index d47199a0f1c1e..06be7c6759bc0 100644 --- a/x-pack/test/api_integration/apis/search/session.ts +++ b/x-pack/test/api_integration/apis/search/session.ts @@ -403,7 +403,12 @@ export default function ({ getService }: FtrProviderContext) { const { id: id1 } = searchRes1.body; // it might take the session a moment to be created - await new Promise((resolve) => setTimeout(resolve, 2500)); + await retry.waitFor('search session created', async () => { + const response = await supertest + .get(`/internal/session/${sessionId}`) + .set('kbn-xsrf', 'foo'); + return response.body.statusCode === undefined; + }); const getSessionFirstTime = await supertest .get(`/internal/session/${sessionId}`) From 0f699fd013b984f7d567425cade00c6f511f499f Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 10:29:15 -0400 Subject: [PATCH 13/43] [Fleet] Improve default port experience in the settings UI (#102982) (#103079) Co-authored-by: Nicolas Chaulet --- .../services/hosts_utils.test.ts | 0 .../services/hosts_utils.ts | 0 x-pack/plugins/fleet/common/services/index.ts | 2 + .../components/settings_flyout/index.tsx | 47 ++++++++++++++----- .../plugins/fleet/server/services/output.ts | 3 +- .../plugins/fleet/server/services/settings.ts | 7 ++- 6 files changed, 42 insertions(+), 17 deletions(-) rename x-pack/plugins/fleet/{server => common}/services/hosts_utils.test.ts (100%) rename x-pack/plugins/fleet/{server => common}/services/hosts_utils.ts (100%) diff --git a/x-pack/plugins/fleet/server/services/hosts_utils.test.ts b/x-pack/plugins/fleet/common/services/hosts_utils.test.ts similarity index 100% rename from x-pack/plugins/fleet/server/services/hosts_utils.test.ts rename to x-pack/plugins/fleet/common/services/hosts_utils.test.ts diff --git a/x-pack/plugins/fleet/server/services/hosts_utils.ts b/x-pack/plugins/fleet/common/services/hosts_utils.ts similarity index 100% rename from x-pack/plugins/fleet/server/services/hosts_utils.ts rename to x-pack/plugins/fleet/common/services/hosts_utils.ts diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts index 86361ae163399..a6f4cd319b970 100644 --- a/x-pack/plugins/fleet/common/services/index.ts +++ b/x-pack/plugins/fleet/common/services/index.ts @@ -30,3 +30,5 @@ export { validationHasErrors, countValidationErrors, } from './validate_package_policy'; + +export { normalizeHostsForAgents } from './hosts_utils'; diff --git a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx b/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx index d748e655bd506..9bc1bc977b786 100644 --- a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx @@ -38,7 +38,7 @@ import { useGetOutputs, sendPutOutput, } from '../../hooks'; -import { isDiffPathProtocol } from '../../../common'; +import { isDiffPathProtocol, normalizeHostsForAgents } from '../../../common'; import { SettingsConfirmModal } from './confirm_modal'; import type { SettingsConfirmModalProps } from './confirm_modal'; @@ -53,8 +53,20 @@ interface Props { onClose: () => void; } -function isSameArrayValue(arrayA: string[] = [], arrayB: string[] = []) { - return arrayA.length === arrayB.length && arrayA.every((val, index) => val === arrayB[index]); +function normalizeHosts(hostsInput: string[]) { + return hostsInput.map((host) => { + try { + return normalizeHostsForAgents(host); + } catch (err) { + return host; + } + }); +} + +function isSameArrayValueWithNormalizedHosts(arrayA: string[] = [], arrayB: string[] = []) { + const hostsA = normalizeHosts(arrayA); + const hostsB = normalizeHosts(arrayB); + return hostsA.length === hostsB.length && hostsA.every((val, index) => val === hostsB[index]); } function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { @@ -234,8 +246,11 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { return false; } return ( - !isSameArrayValue(settings.fleet_server_hosts, inputs.fleetServerHosts.value) || - !isSameArrayValue(output.hosts, inputs.elasticsearchUrl.value) || + !isSameArrayValueWithNormalizedHosts( + settings.fleet_server_hosts, + inputs.fleetServerHosts.value + ) || + !isSameArrayValueWithNormalizedHosts(output.hosts, inputs.elasticsearchUrl.value) || (output.config_yaml || '') !== inputs.additionalYamlConfig.value ); }, [settings, inputs, output]); @@ -246,32 +261,37 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { } const tmpChanges: SettingsConfirmModalProps['changes'] = []; - if (!isSameArrayValue(output.hosts, inputs.elasticsearchUrl.value)) { + if (!isSameArrayValueWithNormalizedHosts(output.hosts, inputs.elasticsearchUrl.value)) { tmpChanges.push( { type: 'elasticsearch', direction: 'removed', - urls: output.hosts || [], + urls: normalizeHosts(output.hosts || []), }, { type: 'elasticsearch', direction: 'added', - urls: inputs.elasticsearchUrl.value, + urls: normalizeHosts(inputs.elasticsearchUrl.value), } ); } - if (!isSameArrayValue(settings.fleet_server_hosts, inputs.fleetServerHosts.value)) { + if ( + !isSameArrayValueWithNormalizedHosts( + settings.fleet_server_hosts, + inputs.fleetServerHosts.value + ) + ) { tmpChanges.push( { type: 'fleet_server', direction: 'removed', - urls: settings.fleet_server_hosts, + urls: normalizeHosts(settings.fleet_server_hosts || []), }, { type: 'fleet_server', direction: 'added', - urls: inputs.fleetServerHosts.value, + urls: normalizeHosts(inputs.fleetServerHosts.value), } ); } @@ -300,7 +320,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { helpText={ = ({ onClose }) => { defaultMessage: 'Elasticsearch hosts', })} helpText={i18n.translate('xpack.fleet.settings.elasticsearchUrlsHelpTect', { - defaultMessage: 'Specify the Elasticsearch URLs where agents send data.', + defaultMessage: + 'Specify the Elasticsearch URLs where agents send data. Elasticsearch uses port 9200 by default.', })} /> diff --git a/x-pack/plugins/fleet/server/services/output.ts b/x-pack/plugins/fleet/server/services/output.ts index 0c7b086f78fdf..8c6bc7eca0401 100644 --- a/x-pack/plugins/fleet/server/services/output.ts +++ b/x-pack/plugins/fleet/server/services/output.ts @@ -9,10 +9,9 @@ import type { SavedObjectsClientContract } from 'src/core/server'; import type { NewOutput, Output, OutputSOAttributes } from '../types'; import { DEFAULT_OUTPUT, OUTPUT_SAVED_OBJECT_TYPE } from '../constants'; -import { decodeCloudId } from '../../common'; +import { decodeCloudId, normalizeHostsForAgents } from '../../common'; import { appContextService } from './app_context'; -import { normalizeHostsForAgents } from './hosts_utils'; const SAVED_OBJECT_TYPE = OUTPUT_SAVED_OBJECT_TYPE; diff --git a/x-pack/plugins/fleet/server/services/settings.ts b/x-pack/plugins/fleet/server/services/settings.ts index 226fbb29467c2..26d581f32d9a2 100644 --- a/x-pack/plugins/fleet/server/services/settings.ts +++ b/x-pack/plugins/fleet/server/services/settings.ts @@ -8,11 +8,14 @@ import Boom from '@hapi/boom'; import type { SavedObjectsClientContract } from 'kibana/server'; -import { decodeCloudId, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE } from '../../common'; +import { + decodeCloudId, + GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + normalizeHostsForAgents, +} from '../../common'; import type { SettingsSOAttributes, Settings, BaseSettings } from '../../common'; import { appContextService } from './app_context'; -import { normalizeHostsForAgents } from './hosts_utils'; export async function getSettings(soClient: SavedObjectsClientContract): Promise { const res = await soClient.find({ From f073f0efc35a5d6d6d932c4d8de8c4c2f0e55403 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 11:56:23 -0400 Subject: [PATCH 14/43] Adjust copy for non-removable integrations/packages (#103068) (#103092) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alejandro Fernández Gómez --- .../sections/epm/screens/detail/settings/settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx index 995423ea91f96..9e8d200344b01 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx @@ -233,7 +233,7 @@ export const SettingsPage: React.FC = memo(({ packageInfo }: Props) => { , From a0c15f4dfaca797f2eb529fdeda60c6ff2fe5488 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 12:09:50 -0400 Subject: [PATCH 15/43] [Security Solution][Endpoint] Hide endpoint event filters list in detections tab (#102644) (#103097) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add event filters filter on exception list to hide it in UI * Fixes unit test and added more tests for showEventFilters * fixes test adding showEventFilters test cases * Pass params as js object instead of individual variables Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: David Sánchez --- .../src/typescript_types/index.ts | 1 + .../src/use_exception_lists/index.ts | 7 +- .../get_event_filters_filter/index.test.ts | 39 +++ .../src/get_event_filters_filter/index.ts | 27 ++ .../src/get_filters/index.test.ts | 274 ++++++++++++++++-- .../src/get_filters/index.ts | 24 +- .../hooks/use_exception_lists.test.ts | 89 +++++- .../rules/all/exceptions/exceptions_table.tsx | 1 + 8 files changed, 420 insertions(+), 42 deletions(-) create mode 100644 packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.test.ts create mode 100644 packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.ts diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts index f75f0dcebf4f6..1909bcb1bcc2e 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts @@ -42,6 +42,7 @@ export interface UseExceptionListsProps { notifications: NotificationsStart; pagination?: Pagination; showTrustedApps: boolean; + showEventFilters: boolean; } export interface UseExceptionListProps { diff --git a/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts index a9a93aa8df49a..0bd4c6c705668 100644 --- a/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts +++ b/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts @@ -28,6 +28,7 @@ export type ReturnExceptionLists = [boolean, ExceptionListSchema[], Pagination, * @param namespaceTypes spaces to be searched * @param notifications kibana service for displaying toasters * @param showTrustedApps boolean - include/exclude trusted app lists + * @param showEventFilters boolean - include/exclude event filters lists * @param pagination * */ @@ -43,6 +44,7 @@ export const useExceptionLists = ({ namespaceTypes, notifications, showTrustedApps = false, + showEventFilters = false, }: UseExceptionListsProps): ReturnExceptionLists => { const [exceptionLists, setExceptionLists] = useState([]); const [paginationInfo, setPagination] = useState(pagination); @@ -51,8 +53,9 @@ export const useExceptionLists = ({ const namespaceTypesAsString = useMemo(() => namespaceTypes.join(','), [namespaceTypes]); const filters = useMemo( - (): string => getFilters(filterOptions, namespaceTypes, showTrustedApps), - [namespaceTypes, filterOptions, showTrustedApps] + (): string => + getFilters({ filters: filterOptions, namespaceTypes, showTrustedApps, showEventFilters }), + [namespaceTypes, filterOptions, showTrustedApps, showEventFilters] ); useEffect(() => { diff --git a/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.test.ts b/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.test.ts new file mode 100644 index 0000000000000..934a9cbff56a6 --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.test.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getEventFiltersFilter } from '.'; + +describe('getEventFiltersFilter', () => { + test('it returns filter to search for "exception-list" namespace trusted apps', () => { + const filter = getEventFiltersFilter(true, ['exception-list']); + + expect(filter).toEqual('(exception-list.attributes.list_id: endpoint_event_filters*)'); + }); + + test('it returns filter to search for "exception-list" and "agnostic" namespace trusted apps', () => { + const filter = getEventFiltersFilter(true, ['exception-list', 'exception-list-agnostic']); + + expect(filter).toEqual( + '(exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it returns filter to exclude "exception-list" namespace trusted apps', () => { + const filter = getEventFiltersFilter(false, ['exception-list']); + + expect(filter).toEqual('(not exception-list.attributes.list_id: endpoint_event_filters*)'); + }); + + test('it returns filter to exclude "exception-list" and "agnostic" namespace trusted apps', () => { + const filter = getEventFiltersFilter(false, ['exception-list', 'exception-list-agnostic']); + + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); +}); diff --git a/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.ts b/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.ts new file mode 100644 index 0000000000000..7e55073228fca --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { SavedObjectType } from '../types'; + +export const getEventFiltersFilter = ( + showEventFilter: boolean, + namespaceTypes: SavedObjectType[] +): string => { + if (showEventFilter) { + const filters = namespaceTypes.map((namespace) => { + return `${namespace}.attributes.list_id: ${ENDPOINT_EVENT_FILTERS_LIST_ID}*`; + }); + return `(${filters.join(' OR ')})`; + } else { + const filters = namespaceTypes.map((namespace) => { + return `not ${namespace}.attributes.list_id: ${ENDPOINT_EVENT_FILTERS_LIST_ID}*`; + }); + return `(${filters.join(' AND ')})`; + } +}; diff --git a/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts b/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts index 327a29dc1b987..bfaad52ee8147 100644 --- a/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts +++ b/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts @@ -11,106 +11,318 @@ import { getFilters } from '.'; describe('getFilters', () => { describe('single', () => { test('it properly formats when no filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({}, ['single'], false); + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single'], + showTrustedApps: false, + showEventFilters: false, + }); - expect(filter).toEqual('(not exception-list.attributes.list_id: endpoint_trusted_apps*)'); + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + ); }); test('it properly formats when no filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({}, ['single'], true); + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single'], + showTrustedApps: true, + showEventFilters: false, + }); - expect(filter).toEqual('(exception-list.attributes.list_id: endpoint_trusted_apps*)'); + expect(filter).toEqual( + '(exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + ); }); test('it properly formats when filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['single'], false); + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single'], + showTrustedApps: false, + showEventFilters: false, + }); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' ); }); test('it if filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['single'], true); + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single'], + showTrustedApps: true, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when no filters passed and "showEventFilters" is false', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single'], + showTrustedApps: false, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when no filters passed and "showEventFilters" is true', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single'], + showTrustedApps: false, + showEventFilters: true, + }); + + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when filters passed and "showEventFilters" is false', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single'], + showTrustedApps: false, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it if filters passed and "showEventFilters" is true', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single'], + showTrustedApps: false, + showEventFilters: true, + }); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters*)' ); }); }); describe('agnostic', () => { test('it properly formats when no filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({}, ['agnostic'], false); + const filter = getFilters({ + filters: {}, + namespaceTypes: ['agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); expect(filter).toEqual( - '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); test('it properly formats when no filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({}, ['agnostic'], true); + const filter = getFilters({ + filters: {}, + namespaceTypes: ['agnostic'], + showTrustedApps: true, + showEventFilters: false, + }); expect(filter).toEqual( - '(exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); test('it properly formats when filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['agnostic'], false); + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); expect(filter).toEqual( - '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); test('it if filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['agnostic'], true); + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['agnostic'], + showTrustedApps: true, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when no filters passed and "showEventFilters" is false', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when no filters passed and "showEventFilters" is true', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['agnostic'], + showTrustedApps: false, + showEventFilters: true, + }); + + expect(filter).toEqual( + '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when filters passed and "showEventFilters" is false', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it if filters passed and "showEventFilters" is true', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['agnostic'], + showTrustedApps: false, + showEventFilters: true, + }); expect(filter).toEqual( - '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); }); describe('single, agnostic', () => { test('it properly formats when no filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({}, ['single', 'agnostic'], false); + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); expect(filter).toEqual( - '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); test('it properly formats when no filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({}, ['single', 'agnostic'], true); + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: true, + showEventFilters: false, + }); expect(filter).toEqual( - '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); test('it properly formats when filters passed and "showTrustedApps" is false', () => { - const filter = getFilters( - { created_by: 'moi', name: 'Sample' }, - ['single', 'agnostic'], - false - ); + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); test('it properly formats when filters passed and "showTrustedApps" is true', () => { - const filter = getFilters( - { created_by: 'moi', name: 'Sample' }, - ['single', 'agnostic'], - true + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: true, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when no filters passed and "showEventFilters" is false', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when no filters passed and "showEventFilters" is true', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: false, + showEventFilters: true, + }); + + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when filters passed and "showEventFilters" is false', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); + }); + + test('it properly formats when filters passed and "showEventFilters" is true', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: false, + showEventFilters: true, + }); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); }); diff --git a/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts b/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts index c9dd6ccae484c..238ae5541343c 100644 --- a/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts +++ b/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts @@ -10,14 +10,26 @@ import { ExceptionListFilter, NamespaceType } from '@kbn/securitysolution-io-ts- import { getGeneralFilters } from '../get_general_filters'; import { getSavedObjectTypes } from '../get_saved_object_types'; import { getTrustedAppsFilter } from '../get_trusted_apps_filter'; +import { getEventFiltersFilter } from '../get_event_filters_filter'; -export const getFilters = ( - filters: ExceptionListFilter, - namespaceTypes: NamespaceType[], - showTrustedApps: boolean -): string => { +export interface GetFiltersParams { + filters: ExceptionListFilter; + namespaceTypes: NamespaceType[]; + showTrustedApps: boolean; + showEventFilters: boolean; +} + +export const getFilters = ({ + filters, + namespaceTypes, + showTrustedApps, + showEventFilters, +}: GetFiltersParams): string => { const namespaces = getSavedObjectTypes({ namespaceType: namespaceTypes }); const generalFilters = getGeneralFilters(filters, namespaces); const trustedAppsFilter = getTrustedAppsFilter(showTrustedApps, namespaces); - return [generalFilters, trustedAppsFilter].filter((filter) => filter.trim() !== '').join(' AND '); + const eventFiltersFilter = getEventFiltersFilter(showEventFilters, namespaces); + return [generalFilters, trustedAppsFilter, eventFiltersFilter] + .filter((filter) => filter.trim() !== '') + .join(' AND '); }; diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts index bdcb4224eed9c..4987de321c556 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts @@ -48,6 +48,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }) ); @@ -83,6 +84,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }) ); @@ -122,6 +124,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: true, }) ); @@ -132,7 +135,7 @@ describe('useExceptionLists', () => { expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ filters: - '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)', + '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)', http: mockKibanaHttpService, namespaceTypes: 'single,agnostic', pagination: { page: 1, perPage: 20 }, @@ -157,6 +160,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }) ); @@ -167,7 +171,79 @@ describe('useExceptionLists', () => { expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ filters: - '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)', + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)', + http: mockKibanaHttpService, + namespaceTypes: 'single,agnostic', + pagination: { page: 1, perPage: 20 }, + signal: new AbortController().signal, + }); + }); + }); + + test('fetches event filters lists if "showEventFilters" is true', async () => { + const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists'); + + await act(async () => { + const { waitForNextUpdate } = renderHook(() => + useExceptionLists({ + errorMessage: 'Uh oh', + filterOptions: {}, + http: mockKibanaHttpService, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + showEventFilters: true, + showTrustedApps: false, + }) + ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ + filters: + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)', + http: mockKibanaHttpService, + namespaceTypes: 'single,agnostic', + pagination: { page: 1, perPage: 20 }, + signal: new AbortController().signal, + }); + }); + }); + + test('does not fetch event filters lists if "showEventFilters" is false', async () => { + const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists'); + + await act(async () => { + const { waitForNextUpdate } = renderHook(() => + useExceptionLists({ + errorMessage: 'Uh oh', + filterOptions: {}, + http: mockKibanaHttpService, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + showEventFilters: false, + showTrustedApps: false, + }) + ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ + filters: + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)', http: mockKibanaHttpService, namespaceTypes: 'single,agnostic', pagination: { page: 1, perPage: 20 }, @@ -195,6 +271,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }) ); @@ -205,7 +282,7 @@ describe('useExceptionLists', () => { expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ filters: - '(exception-list.attributes.created_by:Moi OR exception-list-agnostic.attributes.created_by:Moi) AND (exception-list.attributes.name.text:Sample Endpoint OR exception-list-agnostic.attributes.name.text:Sample Endpoint) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)', + '(exception-list.attributes.created_by:Moi OR exception-list-agnostic.attributes.created_by:Moi) AND (exception-list.attributes.name.text:Sample Endpoint OR exception-list-agnostic.attributes.name.text:Sample Endpoint) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)', http: mockKibanaHttpService, namespaceTypes: 'single,agnostic', pagination: { page: 1, perPage: 20 }, @@ -228,6 +305,7 @@ describe('useExceptionLists', () => { namespaceTypes, notifications, pagination, + showEventFilters, showTrustedApps, }) => useExceptionLists({ @@ -237,6 +315,7 @@ describe('useExceptionLists', () => { namespaceTypes, notifications, pagination, + showEventFilters, showTrustedApps, }), { @@ -251,6 +330,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }, } @@ -271,6 +351,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }); // NOTE: Only need one call here because hook already initilaized @@ -298,6 +379,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }) ); @@ -336,6 +418,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }) ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index 7f734b10fd020..19a3b0b38e53a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -78,6 +78,7 @@ export const ExceptionListsTable = React.memo( namespaceTypes: ['single', 'agnostic'], notifications, showTrustedApps: false, + showEventFilters: false, }); const [loadingTableInfo, exceptionListsWithRuleRefs, exceptionsListsRef] = useAllExceptionLists( { From 6a8007bb694537be42ad36bf18a9fc8659358b47 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 12:25:30 -0400 Subject: [PATCH 16/43] Adds a versioned class name to a root DOM element (#102443) (#103101) Co-authored-by: Christiane (Tina) Heiligers --- src/core/public/chrome/chrome_service.test.ts | 54 ++++++++++++++++++- src/core/public/chrome/chrome_service.tsx | 13 +++++ src/core/public/core_system.test.ts | 7 +-- src/core/public/core_system.ts | 8 +-- src/core/public/public.api.md | 2 +- 5 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index 0264c8a1acf75..92f5a854f6b00 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -53,8 +53,21 @@ function defaultStartDeps(availableApps?: App[]) { return deps; } +function defaultStartTestOptions({ + browserSupportsCsp = true, + kibanaVersion = 'version', +}: { + browserSupportsCsp?: boolean; + kibanaVersion?: string; +}): any { + return { + browserSupportsCsp, + kibanaVersion, + }; +} + async function start({ - options = { browserSupportsCsp: true }, + options = defaultStartTestOptions({}), cspConfigMock = { warnLegacyBrowsers: true }, startDeps = defaultStartDeps(), }: { options?: any; cspConfigMock?: any; startDeps?: ReturnType } = {}) { @@ -82,7 +95,9 @@ afterAll(() => { describe('start', () => { it('adds legacy browser warning if browserSupportsCsp is disabled and warnLegacyBrowsers is enabled', async () => { - const { startDeps } = await start({ options: { browserSupportsCsp: false } }); + const { startDeps } = await start({ + options: { browserSupportsCsp: false, kibanaVersion: '7.0.0' }, + }); expect(startDeps.notifications.toasts.addWarning.mock.calls).toMatchInlineSnapshot(` Array [ @@ -95,6 +110,41 @@ describe('start', () => { `); }); + it('adds the kibana versioned class to the document body', async () => { + const { chrome, service } = await start({ + options: { browserSupportsCsp: false, kibanaVersion: '1.2.3' }, + }); + const promise = chrome.getBodyClasses$().pipe(toArray()).toPromise(); + service.stop(); + await expect(promise).resolves.toMatchInlineSnapshot(` + Array [ + Array [ + "kbnBody", + "kbnBody--noHeaderBanner", + "kbnBody--chromeHidden", + "kbnVersion-1-2-3", + ], + ] + `); + }); + it('strips off "snapshot" from the kibana version if present', async () => { + const { chrome, service } = await start({ + options: { browserSupportsCsp: false, kibanaVersion: '8.0.0-SnAPshot' }, + }); + const promise = chrome.getBodyClasses$().pipe(toArray()).toPromise(); + service.stop(); + await expect(promise).resolves.toMatchInlineSnapshot(` + Array [ + Array [ + "kbnBody", + "kbnBody--noHeaderBanner", + "kbnBody--chromeHidden", + "kbnVersion-8-0-0", + ], + ] + `); + }); + it('does not add legacy browser warning if browser supports CSP', async () => { const { startDeps } = await start(); diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 5ed447edde75a..f1381c52ce779 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -37,9 +37,11 @@ import { export type { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; const IS_LOCKED_KEY = 'core.chrome.isLocked'; +const SNAPSHOT_REGEX = /-snapshot/i; interface ConstructorParams { browserSupportsCsp: boolean; + kibanaVersion: string; } interface StartDeps { @@ -116,6 +118,16 @@ export class ChromeService { const helpSupportUrl$ = new BehaviorSubject(KIBANA_ASK_ELASTIC_LINK); const isNavDrawerLocked$ = new BehaviorSubject(localStorage.getItem(IS_LOCKED_KEY) === 'true'); + const getKbnVersionClass = () => { + // we assume that the version is valid and has the form 'X.X.X' + // strip out `SNAPSHOT` and reformat to 'X-X-X' + const formattedVersionClass = this.params.kibanaVersion + .replace(SNAPSHOT_REGEX, '') + .split('.') + .join('-'); + return `kbnVersion-${formattedVersionClass}`; + }; + const headerBanner$ = new BehaviorSubject(undefined); const bodyClasses$ = combineLatest([headerBanner$, this.isVisible$!]).pipe( map(([headerBanner, isVisible]) => { @@ -123,6 +135,7 @@ export class ChromeService { 'kbnBody', headerBanner ? 'kbnBody--hasHeaderBanner' : 'kbnBody--noHeaderBanner', isVisible ? 'kbnBody--chromeVisible' : 'kbnBody--chromeHidden', + getKbnVersionClass(), ]; }) ); diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 1c4e78f0a5c2e..8ead0f50785bd 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -46,6 +46,7 @@ const defaultCoreSystemParams = { csp: { warnLegacyBrowsers: true, }, + version: 'version', } as any, }; @@ -91,12 +92,12 @@ describe('constructor', () => { }); }); - it('passes browserSupportsCsp to ChromeService', () => { + it('passes browserSupportsCsp and coreContext to ChromeService', () => { createCoreSystem(); - expect(ChromeServiceConstructor).toHaveBeenCalledTimes(1); expect(ChromeServiceConstructor).toHaveBeenCalledWith({ - browserSupportsCsp: expect.any(Boolean), + browserSupportsCsp: true, + kibanaVersion: 'version', }); }); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index f0ea1e62fc33f..9a28bf45df927 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -5,7 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import { CoreId } from '../server'; import { PackageInfo, EnvironmentMode } from '../server/types'; import { CoreSetup, CoreStart } from '.'; @@ -98,6 +97,7 @@ export class CoreSystem { this.injectedMetadata = new InjectedMetadataService({ injectedMetadata, }); + this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env }; this.fatalErrors = new FatalErrorsService(rootDomElement, () => { // Stop Core before rendering any fatal errors into the DOM @@ -109,14 +109,16 @@ export class CoreSystem { this.savedObjects = new SavedObjectsService(); this.uiSettings = new UiSettingsService(); this.overlay = new OverlayService(); - this.chrome = new ChromeService({ browserSupportsCsp }); + this.chrome = new ChromeService({ + browserSupportsCsp, + kibanaVersion: injectedMetadata.version, + }); this.docLinks = new DocLinksService(); this.rendering = new RenderingService(); this.application = new ApplicationService(); this.integrations = new IntegrationsService(); this.deprecations = new DeprecationsService(); - this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env }; this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins); this.coreApp = new CoreApp(this.coreContext); } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 31e85341fb519..ca95b253f9cdb 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1632,6 +1632,6 @@ export interface UserProvidedValues { // Warnings were encountered during analysis: // -// src/core/public/core_system.ts:166:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts +// src/core/public/core_system.ts:168:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts ``` From a3286bbbd74d33c41e59a07089f08004c54d4142 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 23 Jun 2021 10:43:52 -0600 Subject: [PATCH 17/43] [Maps] show radius when drawing distance filter (#102808) (#103104) * [Maps] show radius when drawing distance filter * show more precision when radius is between 10km and 1km * move radius display from line to left of cursor Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../mb_map/draw_control/draw_circle.ts | 66 ++++++++++++++++--- .../mb_map/draw_control/draw_control.tsx | 24 ++++++- 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts index f0df797582bef..998329a78bfbb 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts @@ -11,7 +11,11 @@ import turfDistance from '@turf/distance'; // @ts-expect-error import turfCircle from '@turf/circle'; -import { Position } from 'geojson'; +import { Feature, GeoJSON, Position } from 'geojson'; + +const DRAW_CIRCLE_RADIUS = 'draw-circle-radius'; + +export const DRAW_CIRCLE_RADIUS_MB_FILTER = ['==', 'meta', DRAW_CIRCLE_RADIUS]; export interface DrawCircleProperties { center: Position; @@ -22,10 +26,12 @@ type DrawCircleState = { circle: { properties: Omit & { center: Position | null; + edge: Position | null; + radiusKm: number; }; id: string | number; incomingCoords: (coords: unknown[]) => void; - toGeoJSON: () => unknown; + toGeoJSON: () => GeoJSON; }; }; @@ -43,6 +49,7 @@ export const DrawCircle = { type: 'Feature', properties: { center: null, + edge: null, radiusKm: 0, }, geometry: { @@ -96,6 +103,7 @@ export const DrawCircle = { } const mouseLocation = [e.lngLat.lng, e.lngLat.lat]; + state.circle.properties.edge = mouseLocation; state.circle.properties.radiusKm = turfDistance(state.circle.properties.center, mouseLocation); const newCircleFeature = turfCircle( state.circle.properties.center, @@ -124,15 +132,53 @@ export const DrawCircle = { this.changeMode('simple_select', {}, { silent: true }); } }, - toDisplayFeatures( - state: DrawCircleState, - geojson: { properties: { active: string } }, - display: (geojson: unknown) => unknown - ) { - if (state.circle.properties.center) { - geojson.properties.active = 'true'; - return display(geojson); + toDisplayFeatures(state: DrawCircleState, geojson: Feature, display: (geojson: Feature) => void) { + if (!state.circle.properties.center || !state.circle.properties.edge) { + return null; + } + + geojson.properties!.active = 'true'; + + let radiusLabel = ''; + if (state.circle.properties.radiusKm <= 1) { + radiusLabel = `${Math.round(state.circle.properties.radiusKm * 1000)} m`; + } else if (state.circle.properties.radiusKm <= 10) { + radiusLabel = `${state.circle.properties.radiusKm.toFixed(1)} km`; + } else { + radiusLabel = `${Math.round(state.circle.properties.radiusKm)} km`; } + + // display radius label, requires custom 'symbol' style with DRAW_CIRCLE_RADIUS_MB_FILTER filter + display({ + type: 'Feature', + properties: { + meta: DRAW_CIRCLE_RADIUS, + parent: state.circle.id, + radiusLabel, + active: 'false', + }, + geometry: { + type: 'Point', + coordinates: state.circle.properties.edge, + }, + }); + + // display line from center vertex to edge + display({ + type: 'Feature', + properties: { + meta: 'draw-circle-radius-line', + parent: state.circle.id, + active: 'true', + }, + geometry: { + type: 'LineString', + coordinates: [state.circle.properties.center, state.circle.properties.edge], + }, + }); + + // display circle + display(geojson); }, onTrash(state: DrawCircleState) { // @ts-ignore diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx index 879bd85dd6019..5d9cb59bbe522 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx @@ -14,9 +14,11 @@ import DrawRectangle from 'mapbox-gl-draw-rectangle-mode'; import type { Map as MbMap } from '@kbn/mapbox-gl'; import { Feature } from 'geojson'; import { DRAW_SHAPE } from '../../../../common/constants'; -import { DrawCircle } from './draw_circle'; +import { DrawCircle, DRAW_CIRCLE_RADIUS_MB_FILTER } from './draw_circle'; import { DrawTooltip } from './draw_tooltip'; +const GL_DRAW_RADIUS_LABEL_LAYER_ID = 'gl-draw-radius-label'; + const mbModeEquivalencies = new Map([ ['simple_select', DRAW_SHAPE.SIMPLE_SELECT], ['draw_rectangle', DRAW_SHAPE.BOUNDS], @@ -94,6 +96,7 @@ export class DrawControl extends Component { this.props.mbMap.getCanvas().style.cursor = ''; this.props.mbMap.off('draw.modechange', this._onModeChange); this.props.mbMap.off('draw.create', this._onDraw); + this.props.mbMap.removeLayer(GL_DRAW_RADIUS_LABEL_LAYER_ID); this.props.mbMap.removeControl(this._mbDrawControl); this._mbDrawControlAdded = false; } @@ -105,6 +108,25 @@ export class DrawControl extends Component { if (!this._mbDrawControlAdded) { this.props.mbMap.addControl(this._mbDrawControl); + this.props.mbMap.addLayer({ + id: GL_DRAW_RADIUS_LABEL_LAYER_ID, + type: 'symbol', + source: 'mapbox-gl-draw-hot', + filter: DRAW_CIRCLE_RADIUS_MB_FILTER, + layout: { + 'text-anchor': 'right', + 'text-field': '{radiusLabel}', + 'text-size': 16, + 'text-offset': [-1, 0], + 'text-ignore-placement': true, + 'text-allow-overlap': true, + }, + paint: { + 'text-color': '#fbb03b', + 'text-halo-color': 'rgba(255, 255, 255, 1)', + 'text-halo-width': 2, + }, + }); this._mbDrawControlAdded = true; this.props.mbMap.getCanvas().style.cursor = 'crosshair'; this.props.mbMap.on('draw.modechange', this._onModeChange); From 9abcf6e1e8e9407d551be9198a93f14592521515 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 12:47:31 -0400 Subject: [PATCH 18/43] [Discover] Improve flaky test - doc navigation (#102859) (#103108) * [Discover] test flakiness * [Discover] wait for doc loaded * [Discover] update related test * [Discover] clean statement Co-authored-by: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> --- test/functional/apps/discover/_data_grid_doc_navigation.ts | 6 ++++-- test/functional/apps/discover/_doc_navigation.ts | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/test/functional/apps/discover/_data_grid_doc_navigation.ts b/test/functional/apps/discover/_data_grid_doc_navigation.ts index e3e8a20b693f8..cf5532aa6d762 100644 --- a/test/functional/apps/discover/_data_grid_doc_navigation.ts +++ b/test/functional/apps/discover/_data_grid_doc_navigation.ts @@ -41,8 +41,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await rowActions[0].click(); }); - const hasDocHit = await testSubjects.exists('doc-hit'); - expect(hasDocHit).to.be(true); + await retry.waitFor('hit loaded', async () => { + const hasDocHit = await testSubjects.exists('doc-hit'); + return !!hasDocHit; + }); }); // no longer relevant as null field won't be returned in the Fields API response diff --git a/test/functional/apps/discover/_doc_navigation.ts b/test/functional/apps/discover/_doc_navigation.ts index 771dac4d40a64..8d156cb305586 100644 --- a/test/functional/apps/discover/_doc_navigation.ts +++ b/test/functional/apps/discover/_doc_navigation.ts @@ -51,8 +51,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await rowActions[1].click(); }); - const hasDocHit = await testSubjects.exists('doc-hit'); - expect(hasDocHit).to.be(true); + await retry.waitFor('hit loaded', async () => { + const hasDocHit = await testSubjects.exists('doc-hit'); + return !!hasDocHit; + }); }); // no longer relevant as null field won't be returned in the Fields API response From d063e684d7065ab5f3377b3c49fd5df7ee309a31 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 12:50:28 -0400 Subject: [PATCH 19/43] [Maps] remove undefined from map embeddable by_value URL (#102949) (#103109) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Nathan Reese --- x-pack/plugins/maps/common/constants.ts | 11 +++++------ .../plugins/maps/public/embeddable/map_embeddable.tsx | 10 +++++----- .../maps/public/routes/map_page/map_app/map_app.tsx | 4 ++-- x-pack/plugins/maps/server/plugin.ts | 8 ++++---- x-pack/plugins/maps/server/saved_objects/map.ts | 4 ++-- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 37a8e8063c4ed..fa065e701184e 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -58,15 +58,14 @@ export const KBN_IS_CENTROID_FEATURE = '__kbn_is_centroid_feature__'; export const MVT_TOKEN_PARAM_NAME = 'token'; -const MAP_BASE_URL = `/${MAPS_APP_PATH}/${MAP_PATH}`; export function getNewMapPath() { - return MAP_BASE_URL; + return `/${MAPS_APP_PATH}/${MAP_PATH}`; } -export function getExistingMapPath(id: string) { - return `${MAP_BASE_URL}/${id}`; +export function getFullPath(id: string | undefined) { + return `/${MAPS_APP_PATH}${getEditPath(id)}`; } -export function getEditPath(id: string) { - return `/${MAP_PATH}/${id}`; +export function getEditPath(id: string | undefined) { + return id ? `/${MAP_PATH}/${id}` : `/${MAP_PATH}`; } export enum LAYER_TYPE { diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index 5a477754683e6..509cece671dd6 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -54,9 +54,9 @@ import { } from '../selectors/map_selectors'; import { APP_ID, - getExistingMapPath, + getEditPath, + getFullPath, MAP_SAVED_OBJECT_TYPE, - MAP_PATH, RawValue, } from '../../common/constants'; import { RenderToolTipContent } from '../classes/tooltips/tooltip_property'; @@ -180,13 +180,13 @@ export class MapEmbeddable : ''; const input = this.getInput(); const title = input.hidePanelTitles ? '' : input.title || savedMapTitle; - const savedObjectId = (input as MapByReferenceInput).savedObjectId; + const savedObjectId = 'savedObjectId' in input ? input.savedObjectId : undefined; this.updateOutput({ ...this.getOutput(), defaultTitle: savedMapTitle, title, - editPath: `/${MAP_PATH}/${savedObjectId}`, - editUrl: getHttp().basePath.prepend(getExistingMapPath(savedObjectId)), + editPath: getEditPath(savedObjectId), + editUrl: getHttp().basePath.prepend(getFullPath(savedObjectId)), indexPatterns: await this._getIndexPatterns(), }); } diff --git a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx index 0dfff5a2c221e..92459ed28ab91 100644 --- a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx @@ -44,7 +44,7 @@ import { getTopNavConfig } from '../top_nav_config'; import { MapQuery } from '../../../../common/descriptor_types'; import { goToSpecifiedPath } from '../../../render_app'; import { MapSavedObjectAttributes } from '../../../../common/map_saved_object_type'; -import { getExistingMapPath, APP_ID } from '../../../../common/constants'; +import { getFullPath, APP_ID } from '../../../../common/constants'; import { getInitialQuery, getInitialRefreshConfig, @@ -356,7 +356,7 @@ export class MapApp extends React.Component { const savedObjectId = this.props.savedMap.getSavedObjectId(); if (savedObjectId) { getCoreChrome().recentlyAccessed.add( - getExistingMapPath(savedObjectId), + getFullPath(savedObjectId), this.props.savedMap.getTitle(), savedObjectId ); diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index c753297932037..b8676559a4e2b 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -22,7 +22,7 @@ import { getFlightsSavedObjects } from './sample_data/flights_saved_objects.js'; // @ts-ignore import { getWebLogsSavedObjects } from './sample_data/web_logs_saved_objects.js'; import { registerMapsUsageCollector } from './maps_telemetry/collectors/register'; -import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, getExistingMapPath } from '../common/constants'; +import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, getFullPath } from '../common/constants'; import { mapSavedObjects, mapsTelemetrySavedObjects } from './saved_objects'; import { MapsXPackConfig } from '../config'; // @ts-ignore @@ -77,7 +77,7 @@ export class MapsPlugin implements Plugin { home.sampleData.addAppLinksToSampleDataset('ecommerce', [ { - path: getExistingMapPath('2c9c1f60-1909-11e9-919b-ffe5949a18d2'), + path: getFullPath('2c9c1f60-1909-11e9-919b-ffe5949a18d2'), label: sampleDataLinkLabel, icon: APP_ICON, }, @@ -99,7 +99,7 @@ export class MapsPlugin implements Plugin { home.sampleData.addAppLinksToSampleDataset('flights', [ { - path: getExistingMapPath('5dd88580-1906-11e9-919b-ffe5949a18d2'), + path: getFullPath('5dd88580-1906-11e9-919b-ffe5949a18d2'), label: sampleDataLinkLabel, icon: APP_ICON, }, @@ -120,7 +120,7 @@ export class MapsPlugin implements Plugin { home.sampleData.addSavedObjectsToSampleDataset('logs', getWebLogsSavedObjects()); home.sampleData.addAppLinksToSampleDataset('logs', [ { - path: getExistingMapPath('de71f4f0-1902-11e9-919b-ffe5949a18d2'), + path: getFullPath('de71f4f0-1902-11e9-919b-ffe5949a18d2'), label: sampleDataLinkLabel, icon: APP_ICON, }, diff --git a/x-pack/plugins/maps/server/saved_objects/map.ts b/x-pack/plugins/maps/server/saved_objects/map.ts index 78f70e27b2b7b..24effd651a31b 100644 --- a/x-pack/plugins/maps/server/saved_objects/map.ts +++ b/x-pack/plugins/maps/server/saved_objects/map.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsType } from 'src/core/server'; -import { APP_ICON, getExistingMapPath } from '../../common/constants'; +import { APP_ICON, getFullPath } from '../../common/constants'; // @ts-ignore import { savedObjectMigrations } from './saved_object_migrations'; @@ -34,7 +34,7 @@ export const mapSavedObjects: SavedObjectsType = { }, getInAppUrl(obj) { return { - path: getExistingMapPath(obj.id), + path: getFullPath(obj.id), uiCapabilitiesPath: 'maps.show', }; }, From 3d69df2d32acb6f92c3d0385f12741b220dcc988 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Wed, 23 Jun 2021 19:57:39 +0300 Subject: [PATCH 20/43] [Visualize] Adds an info icon tip to the update button (#101469) (#103112) * [Visualize] Adds an info tooltip to the update button * Add iconTip to the visEditor update button * Move to the left and change the icon * Update test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/sidebar/controls.tsx | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx b/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx index a24673a4c1245..e757b5fe8f61d 100644 --- a/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx +++ b/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx @@ -7,7 +7,14 @@ */ import React, { useCallback, useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, + EuiToolTip, + EuiIconTip, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import useDebounce from 'react-use/lib/useDebounce'; @@ -84,19 +91,32 @@ function DefaultEditorControls({ ) : ( - - - + + + + + + + + + + )}
From eea0addbb7ae2d768a8b82cf0e54251f36ece600 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 12:59:02 -0400 Subject: [PATCH 21/43] [Discover] Unskip and improve empty results query functional test (#102995) (#103110) Co-authored-by: Matthias Wilhelm --- test/functional/apps/discover/_discover.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts index 53682cdb2bf30..75bf9372d4d49 100644 --- a/test/functional/apps/discover/_discover.ts +++ b/test/functional/apps/discover/_discover.ts @@ -185,8 +185,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/89550 - describe.skip('query #2, which has an empty time range', () => { + describe('query #2, which has an empty time range', () => { const fromTime = 'Jun 11, 1999 @ 09:22:11.000'; const toTime = 'Jun 12, 1999 @ 11:21:04.000'; @@ -197,8 +196,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should show "no results"', async () => { - const isVisible = await PageObjects.discover.hasNoResults(); - expect(isVisible).to.be(true); + await retry.waitFor('no results screen is displayed', async function () { + const isVisible = await PageObjects.discover.hasNoResults(); + return isVisible === true; + }); }); it('should suggest a new time range is picked', async () => { From b859c36ef666be3212ba26ecc4556593449e602e Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Wed, 23 Jun 2021 19:21:24 +0200 Subject: [PATCH 22/43] [Discover] Dont trigger onChange when the same index pattern is selected (#102652) (#103116) --- .../sidebar/change_indexpattern.test.tsx | 71 +++++++++++++++++++ .../sidebar/change_indexpattern.tsx | 12 ++-- 2 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.test.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.test.tsx new file mode 100644 index 0000000000000..8c32942740a76 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { EuiSelectable } from '@elastic/eui'; +import { ShallowWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { shallowWithIntl } from '@kbn/test/jest'; +import { ChangeIndexPattern } from './change_indexpattern'; +import { indexPatternMock } from '../../../../../__mocks__/index_pattern'; +import { indexPatternWithTimefieldMock } from '../../../../../__mocks__/index_pattern_with_timefield'; +import { IndexPatternRef } from './types'; + +function getProps() { + return { + indexPatternId: indexPatternMock.id, + indexPatternRefs: [ + indexPatternMock as IndexPatternRef, + indexPatternWithTimefieldMock as IndexPatternRef, + ], + onChangeIndexPattern: jest.fn(), + trigger: { + label: indexPatternMock.title, + title: indexPatternMock.title, + 'data-test-subj': 'indexPattern-switch-link', + }, + }; +} + +function getIndexPatternPickerList(instance: ShallowWrapper) { + return instance.find(EuiSelectable).first(); +} + +function getIndexPatternPickerOptions(instance: ShallowWrapper) { + return getIndexPatternPickerList(instance).prop('options'); +} + +export function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: string) { + const options: Array<{ label: string; checked?: 'on' | 'off' }> = getIndexPatternPickerOptions( + instance + ).map((option: { label: string }) => + option.label === selectedLabel + ? { ...option, checked: 'on' } + : { ...option, checked: undefined } + ); + return getIndexPatternPickerList(instance).prop('onChange')!(options); +} + +describe('ChangeIndexPattern', () => { + test('switching index pattern to the same index pattern does not trigger onChangeIndexPattern', async () => { + const props = getProps(); + const comp = shallowWithIntl(); + await act(async () => { + selectIndexPatternPickerOption(comp, indexPatternMock.title); + }); + expect(props.onChangeIndexPattern).toHaveBeenCalledTimes(0); + }); + test('switching index pattern to a different index pattern triggers onChangeIndexPattern', async () => { + const props = getProps(); + const comp = shallowWithIntl(); + await act(async () => { + selectIndexPatternPickerOption(comp, indexPatternWithTimefieldMock.title); + }); + expect(props.onChangeIndexPattern).toHaveBeenCalledTimes(1); + expect(props.onChangeIndexPattern).toHaveBeenCalledWith(indexPatternWithTimefieldMock.id); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx index d5076e4daa990..5f2f35e2419dd 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx @@ -26,17 +26,17 @@ export type ChangeIndexPatternTriggerProps = EuiButtonProps & { // TODO: refactor to shared component with ../../../../../../../../x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern export function ChangeIndexPattern({ - indexPatternRefs, indexPatternId, + indexPatternRefs, onChangeIndexPattern, - trigger, selectableProps, + trigger, }: { - trigger: ChangeIndexPatternTriggerProps; + indexPatternId?: string; indexPatternRefs: IndexPatternRef[]; onChangeIndexPattern: (newId: string) => void; - indexPatternId?: string; selectableProps?: EuiSelectableProps<{ value: string }>; + trigger: ChangeIndexPatternTriggerProps; }) { const [isPopoverOpen, setPopoverIsOpen] = useState(false); @@ -86,7 +86,9 @@ export function ChangeIndexPattern({ const choice = (choices.find(({ checked }) => checked) as unknown) as { value: string; }; - onChangeIndexPattern(choice.value); + if (choice.value !== indexPatternId) { + onChangeIndexPattern(choice.value); + } setPopoverIsOpen(false); }} searchProps={{ From d1b5ebb2b761f202b5b37d9961e8fda61734bdd5 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 23 Jun 2021 20:50:09 +0300 Subject: [PATCH 23/43] [TSVB] Weird behavior after upgrading to 7.13.1 #101590 (#102942) (#103120) Closes: #101590 --- .../public/application/components/aggs/field_select.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx index 7d42eb3f40ac5..610b4a91cfd14 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx @@ -128,7 +128,7 @@ export function FieldSelect({ selectedOptions = [{ label: value!, id: 'INVALID_FIELD' }]; } } else { - if (value && !selectedOptions.length) { + if (value && fields[fieldsSelector] && !selectedOptions.length) { onChange([]); } } From 6e6cdb06536661600de67926cb10898744a32bc3 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 23 Jun 2021 20:50:48 +0300 Subject: [PATCH 24/43] [TSVB] fix tooltip on annotations with 's are not displayed correctly (#102892) (#103121) * [TSVB] tooltip on annotations with 's are not displayed correctly Closes: #102631 * 'handlebars/dist/handlebars' -> 'handlebars' --- .../components/lib/{reorder.js => reorder.ts} | 2 +- ...lace_vars.test.js => replace_vars.test.ts} | 0 .../lib/{replace_vars.js => replace_vars.ts} | 24 +++++++++++++------ .../components/lib/tick_formatter.js | 2 +- .../components/vis_types/timeseries/vis.js | 4 +++- 5 files changed, 22 insertions(+), 10 deletions(-) rename src/plugins/vis_type_timeseries/public/application/components/lib/{reorder.js => reorder.ts} (85%) rename src/plugins/vis_type_timeseries/public/application/components/lib/{replace_vars.test.js => replace_vars.test.ts} (100%) rename src/plugins/vis_type_timeseries/public/application/components/lib/{replace_vars.js => replace_vars.ts} (77%) diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.js b/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.ts similarity index 85% rename from src/plugins/vis_type_timeseries/public/application/components/lib/reorder.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/reorder.ts index 15c21e19af2a5..a026b5bb2051e 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -export const reorder = (list, startIndex, endIndex) => { +export const reorder = (list: unknown[], startIndex: number, endIndex: number) => { const result = Array.from(list); const [removed] = result.splice(startIndex, 1); result.splice(endIndex, 0, removed); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.ts similarity index 77% rename from src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.ts index 458866f2098a0..2862fe933bfb7 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.ts @@ -6,20 +6,30 @@ * Side Public License, v 1. */ -import _ from 'lodash'; -import handlebars from 'handlebars/dist/handlebars'; -import { emptyLabel } from '../../../../common/empty_label'; +import handlebars from 'handlebars'; import { i18n } from '@kbn/i18n'; +import { emptyLabel } from '../../../../common/empty_label'; + +type CompileOptions = Parameters[1]; -export function replaceVars(str, args = {}, vars = {}) { +export function replaceVars( + str: string, + args: Record = {}, + vars: Record = {}, + compileOptions: Partial = {} +) { try { - // we need add '[]' for emptyLabel because this value contains special characters. (https://handlebarsjs.com/guide/expressions.html#literal-segments) + /** we need add '[]' for emptyLabel because this value contains special characters. + * @see (https://handlebarsjs.com/guide/expressions.html#literal-segments) **/ const template = handlebars.compile(str.split(emptyLabel).join(`[${emptyLabel}]`), { strict: true, knownHelpersOnly: true, + ...compileOptions, + }); + const string = template({ + ...vars, + args, }); - - const string = template(_.assign({}, vars, { args })); return string; } catch (e) { diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js index 70529be78567d..c1d82a182e509 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import handlebars from 'handlebars/dist/handlebars'; import { isNumber } from 'lodash'; +import handlebars from 'handlebars'; import { isEmptyValue, DISPLAY_EMPTY_VALUE } from '../../../../common/last_value_utils'; import { inputFormats, outputFormats, isDuration } from '../lib/durations'; import { getFieldFormats } from '../../../services'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index 8e59e8e1bb628..097b0a7b5e332 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -51,7 +51,9 @@ class TimeseriesVisualization extends Component { }; applyDocTo = (template) => (doc) => { - const vars = replaceVars(template, null, doc); + const vars = replaceVars(template, null, doc, { + noEscape: true, + }); if (vars instanceof Error) { this.showToastNotification = vars.error.caused_by; From f4b4d200543bdd5e46b89a4c50bd654ac405674d Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 14:32:17 -0400 Subject: [PATCH 25/43] [Fleet] Support user overrides in composable templates (#101769) (#103126) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Closes #90454 Closes https://github.com/elastic/kibana/issues/72959 * Rename the component templates which are [installed for some packages](https://github.com/elastic/kibana/blob/master/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts#L197-L213) from `${templateName}-mappings` and `${templateName}-settings` to `${templateName}@mappings` and `${templateName}@settings` * When any package is installed, add a component template named `${templateName}@custom` * Any of above templates also include a `_meta` property with `{ package: { name: packageName } }` * On package installation, add any installed component templates to the `installed_es` property of the `epm-packages` saved object * On package removal, remove any installed component templates from the `installed_es` property of the `epm-packages` saved object
Kibana logs showing component templates added for package ``` │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [logs-endpoint.events.file@mappings] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [logs-endpoint.events.registry@mappings] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [.logs-endpoint.diagnostic.collection@mappings] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [logs-endpoint.events.library@mappings] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [logs-endpoint.events.security@mappings] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [logs-endpoint.events.network@mappings] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [logs-endpoint.alerts@mappings] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [metrics-endpoint.metrics@mappings] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [logs-endpoint.events.process@mappings] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [metrics-endpoint.policy@mappings] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [metrics-endpoint.metadata@mappings] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [logs-endpoint.events.registry@custom] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [.logs-endpoint.diagnostic.collection@custom] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [logs-endpoint.events.security@custom] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [logs-endpoint.events.file@custom] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [logs-endpoint.events.library@custom] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [logs-endpoint.events.network@custom] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [logs-endpoint.alerts@custom] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [metrics-endpoint.metrics@custom] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [metrics-endpoint.policy@custom] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [logs-endpoint.events.process@custom] │ info [o.e.c.m.MetadataIndexTemplateService] [JFSIII.local] adding component template [metrics-endpoint.metadata@custom] ```
screenshot - component templates are editable in the Stack Management UI Screen Shot 2021-06-17 at 4 06 24 PM
### Checklist Delete any items that are not applicable to this PR. - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios Co-authored-by: John Schulz --- .../plugins/fleet/common/types/models/epm.ts | 7 +- .../epm/elasticsearch/template/install.ts | 235 +++++++++++------- .../epm/elasticsearch/template/template.ts | 8 +- .../services/epm/packages/_install_package.ts | 7 +- .../server/services/epm/packages/install.ts | 16 +- .../server/services/epm/packages/remove.ts | 51 +++- x-pack/plugins/fleet/server/types/index.tsx | 2 +- .../epm/__snapshots__/install_by_upload.snap | 12 + .../apis/epm/install_by_upload.ts | 4 +- .../apis/epm/install_overrides.ts | 157 ++++++++---- .../apis/epm/install_remove_assets.ts | 65 ++++- .../apis/epm/update_assets.ts | 56 ++++- .../0.1.0/img/logo_overrides_64_color.svg | 7 + .../error_handling/0.1.0/manifest.yml | 6 +- .../0.2.0/img/logo_overrides_64_color.svg | 7 + .../error_handling/0.2.0/manifest.yml | 6 +- x-pack/test/fleet_api_integration/config.ts | 10 +- 17 files changed, 452 insertions(+), 204 deletions(-) create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/img/logo_overrides_64_color.svg create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/img/logo_overrides_64_color.svg diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index aece658083196..c4441fb6e0d95 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { estypes } from '@elastic/elasticsearch'; // Follow pattern from https://github.com/elastic/kibana/pull/52447 // TODO: Update when https://github.com/elastic/kibana/issues/53021 is closed import type { SavedObject, SavedObjectAttributes, SavedObjectReference } from 'src/core/public'; @@ -299,8 +300,8 @@ export interface RegistryDataStream { } export interface RegistryElasticsearch { - 'index_template.settings'?: object; - 'index_template.mappings'?: object; + 'index_template.settings'?: estypes.IndicesIndexSettings; + 'index_template.mappings'?: estypes.MappingTypeMapping; } export interface RegistryDataStreamPermissions { @@ -425,7 +426,7 @@ export interface IndexTemplate { _meta: object; } -export interface TemplateRef { +export interface IndexTemplateEntry { templateName: string; indexTemplate: IndexTemplate; } diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index d202dab54f5bd..db1fba1eedccd 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -11,7 +11,7 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/s import { ElasticsearchAssetType } from '../../../../types'; import type { RegistryDataStream, - TemplateRef, + IndexTemplateEntry, RegistryElasticsearch, InstallablePackage, } from '../../../../types'; @@ -19,7 +19,7 @@ import { loadFieldsFromYaml, processFields } from '../../fields/field'; import type { Field } from '../../fields/field'; import { getPipelineNameForInstallation } from '../ingest_pipeline/install'; import { getAsset, getPathParts } from '../../archive'; -import { removeAssetsFromInstalledEsByType, saveInstalledEsRefs } from '../../packages/install'; +import { removeAssetTypesFromInstalledEs, saveInstalledEsRefs } from '../../packages/install'; import { generateMappings, @@ -34,7 +34,7 @@ export const installTemplates = async ( esClient: ElasticsearchClient, paths: string[], savedObjectsClient: SavedObjectsClientContract -): Promise => { +): Promise => { // install any pre-built index template assets, // atm, this is only the base package's global index templates // Install component templates first, as they are used by the index templates @@ -42,44 +42,36 @@ export const installTemplates = async ( await installPreBuiltTemplates(paths, esClient); // remove package installation's references to index templates - await removeAssetsFromInstalledEsByType( - savedObjectsClient, - installablePackage.name, - ElasticsearchAssetType.indexTemplate - ); + await removeAssetTypesFromInstalledEs(savedObjectsClient, installablePackage.name, [ + ElasticsearchAssetType.indexTemplate, + ElasticsearchAssetType.componentTemplate, + ]); // build templates per data stream from yml files const dataStreams = installablePackage.data_streams; if (!dataStreams) return []; + + const installedTemplatesNested = await Promise.all( + dataStreams.map((dataStream) => + installTemplateForDataStream({ + pkg: installablePackage, + esClient, + dataStream, + }) + ) + ); + const installedTemplates = installedTemplatesNested.flat(); + // get template refs to save - const installedTemplateRefs = dataStreams.map((dataStream) => ({ - id: generateTemplateName(dataStream), - type: ElasticsearchAssetType.indexTemplate, - })); + const installedIndexTemplateRefs = getAllTemplateRefs(installedTemplates); // add package installation's references to index templates - await saveInstalledEsRefs(savedObjectsClient, installablePackage.name, installedTemplateRefs); - - if (dataStreams) { - const installTemplatePromises = dataStreams.reduce>>( - (acc, dataStream) => { - acc.push( - installTemplateForDataStream({ - pkg: installablePackage, - esClient, - dataStream, - }) - ); - return acc; - }, - [] - ); - - const res = await Promise.all(installTemplatePromises); - const installedTemplates = res.flat(); + await saveInstalledEsRefs( + savedObjectsClient, + installablePackage.name, + installedIndexTemplateRefs + ); - return installedTemplates; - } - return []; + return installedTemplates; }; const installPreBuiltTemplates = async (paths: string[], esClient: ElasticsearchClient) => { @@ -160,7 +152,7 @@ export async function installTemplateForDataStream({ pkg: InstallablePackage; esClient: ElasticsearchClient; dataStream: RegistryDataStream; -}): Promise { +}): Promise { const fields = await loadFieldsFromYaml(pkg, dataStream.path); return installTemplate({ esClient, @@ -171,84 +163,118 @@ export async function installTemplateForDataStream({ }); } +interface TemplateMapEntry { + _meta: { package: { name: string } }; + template: + | { + mappings: NonNullable; + } + | { + settings: NonNullable | object; + }; +} +type TemplateMap = Record; function putComponentTemplate( - body: object | undefined, - name: string, - esClient: ElasticsearchClient -): { clusterPromise: Promise; name: string } | undefined { - if (body) { - const esClientParams = { - name, - body, - }; - - return { - // @ts-expect-error body expected to be ClusterPutComponentTemplateRequest - clusterPromise: esClient.cluster.putComponentTemplate(esClientParams, { ignore: [404] }), - name, - }; + esClient: ElasticsearchClient, + params: { + body: TemplateMapEntry; + name: string; + create?: boolean; } +): { clusterPromise: Promise; name: string } { + const { name, body, create = false } = params; + return { + clusterPromise: esClient.cluster.putComponentTemplate( + // @ts-expect-error body is missing required key `settings`. TemplateMapEntry has settings *or* mappings + { name, body, create }, + { ignore: [404] } + ), + name, + }; } -function buildComponentTemplates(registryElasticsearch: RegistryElasticsearch | undefined) { - let mappingsTemplate; - let settingsTemplate; +const mappingsSuffix = '@mappings'; +const settingsSuffix = '@settings'; +const userSettingsSuffix = '@custom'; +type TemplateBaseName = string; +type UserSettingsTemplateName = `${TemplateBaseName}${typeof userSettingsSuffix}`; + +const isUserSettingsTemplate = (name: string): name is UserSettingsTemplateName => + name.endsWith(userSettingsSuffix); + +function buildComponentTemplates(params: { + templateName: string; + registryElasticsearch: RegistryElasticsearch | undefined; + packageName: string; +}) { + const { templateName, registryElasticsearch, packageName } = params; + const mappingsTemplateName = `${templateName}${mappingsSuffix}`; + const settingsTemplateName = `${templateName}${settingsSuffix}`; + const userSettingsTemplateName = `${templateName}${userSettingsSuffix}`; + + const templatesMap: TemplateMap = {}; + const _meta = { package: { name: packageName } }; if (registryElasticsearch && registryElasticsearch['index_template.mappings']) { - mappingsTemplate = { + templatesMap[mappingsTemplateName] = { template: { - mappings: { - ...registryElasticsearch['index_template.mappings'], - }, + mappings: registryElasticsearch['index_template.mappings'], }, + _meta, }; } if (registryElasticsearch && registryElasticsearch['index_template.settings']) { - settingsTemplate = { + templatesMap[settingsTemplateName] = { template: { settings: registryElasticsearch['index_template.settings'], }, + _meta, }; } - return { settingsTemplate, mappingsTemplate }; -} -async function installDataStreamComponentTemplates( - templateName: string, - registryElasticsearch: RegistryElasticsearch | undefined, - esClient: ElasticsearchClient -) { - const templates: string[] = []; - const componentPromises: Array> = []; + // return empty/stub template + templatesMap[userSettingsTemplateName] = { + template: { + settings: {}, + }, + _meta, + }; - const compTemplates = buildComponentTemplates(registryElasticsearch); + return templatesMap; +} - const mappings = putComponentTemplate( - compTemplates.mappingsTemplate, - `${templateName}-mappings`, - esClient - ); +async function installDataStreamComponentTemplates(params: { + templateName: string; + registryElasticsearch: RegistryElasticsearch | undefined; + esClient: ElasticsearchClient; + packageName: string; +}) { + const { templateName, registryElasticsearch, esClient, packageName } = params; + const templates = buildComponentTemplates({ templateName, registryElasticsearch, packageName }); + const templateNames = Object.keys(templates); + const templateEntries = Object.entries(templates); - const settings = putComponentTemplate( - compTemplates.settingsTemplate, - `${templateName}-settings`, - esClient + // TODO: Check return values for errors + await Promise.all( + templateEntries.map(async ([name, body]) => { + if (isUserSettingsTemplate(name)) { + // look for existing user_settings template + const result = await esClient.cluster.getComponentTemplate({ name }, { ignore: [404] }); + const hasUserSettingsTemplate = result.body.component_templates?.length === 1; + if (!hasUserSettingsTemplate) { + // only add if one isn't already present + const { clusterPromise } = putComponentTemplate(esClient, { body, name, create: true }); + return clusterPromise; + } + } else { + const { clusterPromise } = putComponentTemplate(esClient, { body, name }); + return clusterPromise; + } + }) ); - if (mappings) { - templates.push(mappings.name); - componentPromises.push(mappings.clusterPromise); - } - - if (settings) { - templates.push(settings.name); - componentPromises.push(settings.clusterPromise); - } - - // TODO: Check return values for errors - await Promise.all(componentPromises); - return templates; + return templateNames; } export async function installTemplate({ @@ -263,7 +289,7 @@ export async function installTemplate({ dataStream: RegistryDataStream; packageVersion: string; packageName: string; -}): Promise { +}): Promise { const validFields = processFields(fields); const mappings = generateMappings(validFields); const templateName = generateTemplateName(dataStream); @@ -310,11 +336,12 @@ export async function installTemplate({ await esClient.indices.putIndexTemplate(updateIndexTemplateParams, { ignore: [404] }); } - const composedOfTemplates = await installDataStreamComponentTemplates( + const composedOfTemplates = await installDataStreamComponentTemplates({ templateName, - dataStream.elasticsearch, - esClient - ); + registryElasticsearch: dataStream.elasticsearch, + esClient, + packageName, + }); const template = getTemplate({ type: dataStream.type, @@ -342,3 +369,21 @@ export async function installTemplate({ indexTemplate: template, }; } + +export function getAllTemplateRefs(installedTemplates: IndexTemplateEntry[]) { + return installedTemplates.flatMap((installedTemplate) => { + const indexTemplates = [ + { + id: installedTemplate.templateName, + type: ElasticsearchAssetType.indexTemplate, + }, + ]; + const componentTemplates = installedTemplate.indexTemplate.composed_of.map( + (componentTemplateId) => ({ + id: componentTemplateId, + type: ElasticsearchAssetType.componentTemplate, + }) + ); + return indexTemplates.concat(componentTemplates); + }); +} diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts index 07d0df021c827..158996cc574d7 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts @@ -10,7 +10,7 @@ import type { ElasticsearchClient } from 'kibana/server'; import type { Field, Fields } from '../../fields/field'; import type { RegistryDataStream, - TemplateRef, + IndexTemplateEntry, IndexTemplate, IndexTemplateMappings, } from '../../../../types'; @@ -456,7 +456,7 @@ function getBaseTemplate( export const updateCurrentWriteIndices = async ( esClient: ElasticsearchClient, - templates: TemplateRef[] + templates: IndexTemplateEntry[] ): Promise => { if (!templates.length) return; @@ -471,7 +471,7 @@ function isCurrentDataStream(item: CurrentDataStream[] | undefined): item is Cur const queryDataStreamsFromTemplates = async ( esClient: ElasticsearchClient, - templates: TemplateRef[] + templates: IndexTemplateEntry[] ): Promise => { const dataStreamPromises = templates.map((template) => { return getDataStreams(esClient, template); @@ -482,7 +482,7 @@ const queryDataStreamsFromTemplates = async ( const getDataStreams = async ( esClient: ElasticsearchClient, - template: TemplateRef + template: IndexTemplateEntry ): Promise => { const { templateName, indexTemplate } = template; const { body } = await esClient.indices.getDataStream({ name: `${templateName}-*` }); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index 65d71ac5fdc17..1bbbb1bb9b6a2 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -10,10 +10,10 @@ import type { ElasticsearchClient, SavedObject, SavedObjectsClientContract } fro import { MAX_TIME_COMPLETE_INSTALL, ASSETS_SAVED_OBJECT_TYPE } from '../../../../common'; import type { InstallablePackage, InstallSource, PackageAssetReference } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; -import { ElasticsearchAssetType } from '../../../types'; import type { AssetReference, Installation, InstallType } from '../../../types'; import { installTemplates } from '../elasticsearch/template/install'; import { installPipelines, deletePreviousPipelines } from '../elasticsearch/ingest_pipeline/'; +import { getAllTemplateRefs } from '../elasticsearch/template/install'; import { installILMPolicy } from '../elasticsearch/ilm/install'; import { installKibanaAssets, getKibanaAssets } from '../kibana/assets/install'; import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; @@ -170,10 +170,7 @@ export async function _installPackage({ installedPkg.attributes.install_version ); } - const installedTemplateRefs = installedTemplates.map((template) => ({ - id: template.templateName, - type: ElasticsearchAssetType.indexTemplate, - })); + const installedTemplateRefs = getAllTemplateRefs(installedTemplates); // make sure the assets are installed (or didn't error) if (installKibanaAssetsError) throw installKibanaAssetsError; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index c6fd9a8f763ab..e00526cbb4ec4 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -257,8 +257,7 @@ async function installPackageFromRegistry({ const { paths, packageInfo } = await Registry.getRegistryPackage(pkgName, pkgVersion); // try installing the package, if there was an error, call error handler and rethrow - // TODO: without the ts-ignore, TS complains about the type of the value of the returned InstallResult.status - // @ts-ignore + // @ts-expect-error status is string instead of InstallResult.status 'installed' | 'already_installed' return _installPackage({ savedObjectsClient, esClient, @@ -334,8 +333,7 @@ async function installPackageByUpload({ version: packageInfo.version, packageInfo, }); - // TODO: without the ts-ignore, TS complains about the type of the value of the returned InstallResult.status - // @ts-ignore + // @ts-expect-error status is string instead of InstallResult.status 'installed' | 'already_installed' return _installPackage({ savedObjectsClient, esClient, @@ -484,17 +482,17 @@ export const saveInstalledEsRefs = async ( return installedAssets; }; -export const removeAssetsFromInstalledEsByType = async ( +export const removeAssetTypesFromInstalledEs = async ( savedObjectsClient: SavedObjectsClientContract, pkgName: string, - assetType: AssetType + assetTypes: AssetType[] ) => { const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); const installedAssets = installedPkg?.attributes.installed_es; if (!installedAssets?.length) return; - const installedAssetsToSave = installedAssets?.filter(({ id, type }) => { - return type !== assetType; - }); + const installedAssetsToSave = installedAssets?.filter( + (asset) => !assetTypes.includes(asset.type) + ); return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { installed_es: installedAssetsToSave, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index 706f1bbbaaf35..70167d1156a66 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -89,13 +89,18 @@ function deleteKibanaAssets( }); } -function deleteESAssets(installedObjects: EsAssetReference[], esClient: ElasticsearchClient) { +function deleteESAssets( + installedObjects: EsAssetReference[], + esClient: ElasticsearchClient +): Array> { return installedObjects.map(async ({ id, type }) => { const assetType = type as AssetType; if (assetType === ElasticsearchAssetType.ingestPipeline) { return deletePipeline(esClient, id); } else if (assetType === ElasticsearchAssetType.indexTemplate) { - return deleteTemplate(esClient, id); + return deleteIndexTemplate(esClient, id); + } else if (assetType === ElasticsearchAssetType.componentTemplate) { + return deleteComponentTemplate(esClient, id); } else if (assetType === ElasticsearchAssetType.transform) { return deleteTransforms(esClient, [id]); } else if (assetType === ElasticsearchAssetType.dataStreamIlmPolicy) { @@ -111,13 +116,30 @@ async function deleteAssets( ) { const logger = appContextService.getLogger(); - const deletePromises: Array> = [ - ...deleteESAssets(installedEs, esClient), - ...deleteKibanaAssets(installedKibana, savedObjectsClient), - ]; + // must delete index templates first, or component templates which reference them cannot be deleted + // separate the assets into Index Templates and other assets + type Tuple = [EsAssetReference[], EsAssetReference[]]; + const [indexTemplates, otherAssets] = installedEs.reduce( + ([indexAssetTypes, otherAssetTypes], asset) => { + if (asset.type === ElasticsearchAssetType.indexTemplate) { + indexAssetTypes.push(asset); + } else { + otherAssetTypes.push(asset); + } + + return [indexAssetTypes, otherAssetTypes]; + }, + [[], []] + ); try { - await Promise.all(deletePromises); + // must delete index templates first + await Promise.all(deleteESAssets(indexTemplates, esClient)); + // then the other asset types + await Promise.all([ + ...deleteESAssets(otherAssets, esClient), + ...deleteKibanaAssets(installedKibana, savedObjectsClient), + ]); } catch (err) { // in the rollback case, partial installs are likely, so missing assets are not an error if (!savedObjectsClient.errors.isNotFoundError(err)) { @@ -126,13 +148,24 @@ async function deleteAssets( } } -async function deleteTemplate(esClient: ElasticsearchClient, name: string): Promise { +async function deleteIndexTemplate(esClient: ElasticsearchClient, name: string): Promise { // '*' shouldn't ever appear here, but it still would delete all templates if (name && name !== '*') { try { await esClient.indices.deleteIndexTemplate({ name }, { ignore: [404] }); } catch { - throw new Error(`error deleting template ${name}`); + throw new Error(`error deleting index template ${name}`); + } + } +} + +async function deleteComponentTemplate(esClient: ElasticsearchClient, name: string): Promise { + // '*' shouldn't ever appear here, but it still would delete all templates + if (name && name !== '*') { + try { + await esClient.cluster.deleteComponentTemplate({ name }, { ignore: [404] }); + } catch (error) { + throw new Error(`error deleting component template ${name}`); } } } diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index 8927676976457..0c08a09e76f4e 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -63,7 +63,7 @@ export { IndexTemplate, RegistrySearchResults, RegistrySearchResult, - TemplateRef, + IndexTemplateEntry, IndexTemplateMappings, Settings, SettingsSOAttributes, diff --git a/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/install_by_upload.snap b/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/install_by_upload.snap index 7584dfcc8a6c0..13c2dd24f9103 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/install_by_upload.snap +++ b/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/install_by_upload.snap @@ -341,14 +341,26 @@ Object { "id": "logs-apache.access", "type": "index_template", }, + Object { + "id": "logs-apache.access@custom", + "type": "component_template", + }, Object { "id": "metrics-apache.status", "type": "index_template", }, + Object { + "id": "metrics-apache.status@custom", + "type": "component_template", + }, Object { "id": "logs-apache.error", "type": "index_template", }, + Object { + "id": "logs-apache.error@custom", + "type": "component_template", + }, ], "installed_kibana": Array [ Object { diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts b/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts index 71cf7ed79fa2b..182838f21dbda 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts @@ -70,7 +70,7 @@ export default function (providerContext: FtrProviderContext) { .type('application/gzip') .send(buf) .expect(200); - expect(res.body.response.length).to.be(23); + expect(res.body.response.length).to.be(26); }); it('should install a zip archive correctly and package info should return correctly after validation', async function () { @@ -81,7 +81,7 @@ export default function (providerContext: FtrProviderContext) { .type('application/zip') .send(buf) .expect(200); - expect(res.body.response.length).to.be(23); + expect(res.body.response.length).to.be(26); const packageInfoRes = await supertest .get(`/api/fleet/epm/packages/${testPkgKey}`) diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_overrides.ts b/x-pack/test/fleet_api_integration/apis/epm/install_overrides.ts index 1b916dff573af..204ee8508f468 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_overrides.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_overrides.ts @@ -7,22 +7,22 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; -import { warnAndSkipTest } from '../../helpers'; +import { skipIfNoDockerRegistry } from '../../helpers'; -export default function ({ getService }: FtrProviderContext) { +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; const supertest = getService('supertest'); const es = getService('es'); const dockerServers = getService('dockerServers'); - const log = getService('log'); const mappingsPackage = 'overrides-0.1.0'; const server = dockerServers.get('registry'); - const deletePackage = async (pkgkey: string) => { - await supertest.delete(`/api/fleet/epm/packages/${pkgkey}`).set('kbn-xsrf', 'xxxx'); - }; + const deletePackage = async (pkgkey: string) => + supertest.delete(`/api/fleet/epm/packages/${pkgkey}`).set('kbn-xsrf', 'xxxx'); describe('installs packages that include settings and mappings overrides', async () => { + skipIfNoDockerRegistry(providerContext); after(async () => { if (server.enabled) { // remove the package just in case it being installed will affect other tests @@ -31,50 +31,107 @@ export default function ({ getService }: FtrProviderContext) { }); it('should install the overrides package correctly', async function () { - if (server.enabled) { - let { body } = await supertest - .post(`/api/fleet/epm/packages/${mappingsPackage}`) - .set('kbn-xsrf', 'xxxx') - .expect(200); - - const templateName = body.response[0].id; - - ({ body } = await es.transport.request({ - method: 'GET', - path: `/_index_template/${templateName}`, - })); - - // make sure it has the right composed_of array, the contents should be the component templates - // that were installed - expect(body.index_templates[0].index_template.composed_of).to.contain( - `${templateName}-mappings` - ); - expect(body.index_templates[0].index_template.composed_of).to.contain( - `${templateName}-settings` - ); - - ({ body } = await es.transport.request({ - method: 'GET', - path: `/_component_template/${templateName}-mappings`, - })); - - // Make sure that the `dynamic` field exists and is set to false (as it is in the package) - expect(body.component_templates[0].component_template.template.mappings.dynamic).to.be( - false - ); - - ({ body } = await es.transport.request({ - method: 'GET', - path: `/_component_template/${templateName}-settings`, - })); - - // Make sure that the lifecycle name gets set correct in the settings - expect( - body.component_templates[0].component_template.template.settings.index.lifecycle.name - ).to.be('reference'); - } else { - warnAndSkipTest(this, log); - } + let { body } = await supertest + .post(`/api/fleet/epm/packages/${mappingsPackage}`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + + const templateName = body.response[0].id; + + const { body: indexTemplateResponse } = await es.transport.request({ + method: 'GET', + path: `/_index_template/${templateName}`, + }); + + // the index template composed_of has the correct component templates in the correct order + const indexTemplate = indexTemplateResponse.index_templates[0].index_template; + expect(indexTemplate.composed_of).to.eql([ + `${templateName}@mappings`, + `${templateName}@settings`, + `${templateName}@custom`, + ]); + + ({ body } = await es.transport.request({ + method: 'GET', + path: `/_component_template/${templateName}@mappings`, + })); + + // The mappings override provided in the package is set in the mappings component template + expect(body.component_templates[0].component_template.template.mappings.dynamic).to.be(false); + + ({ body } = await es.transport.request({ + method: 'GET', + path: `/_component_template/${templateName}@settings`, + })); + + // The settings override provided in the package is set in the settings component template + expect( + body.component_templates[0].component_template.template.settings.index.lifecycle.name + ).to.be('reference'); + + ({ body } = await es.transport.request({ + method: 'GET', + path: `/_component_template/${templateName}@custom`, + })); + + // The user_settings component template is an empty/stub template at first + const storedTemplate = body.component_templates[0].component_template.template.settings; + expect(storedTemplate).to.eql({}); + + // Update the user_settings component template + ({ body } = await es.transport.request({ + method: 'PUT', + path: `/_component_template/${templateName}@custom`, + body: { + template: { + settings: { + number_of_shards: 3, + index: { + lifecycle: { name: 'overridden by user' }, + number_of_shards: 123, + }, + }, + }, + }, + })); + + // simulate the result + ({ body } = await es.transport.request({ + method: 'POST', + path: `/_index_template/_simulate/${templateName}`, + // body: indexTemplate, // I *think* this should work, but it doesn't + body: { + index_patterns: [`${templateName}-*`], + composed_of: [ + `${templateName}@mappings`, + `${templateName}@settings`, + `${templateName}@custom`, + ], + }, + })); + + expect(body).to.eql({ + template: { + settings: { + index: { + lifecycle: { + name: 'overridden by user', + }, + number_of_shards: '3', + }, + }, + mappings: { + dynamic: 'false', + }, + aliases: {}, + }, + overlapping: [ + { + name: 'logs', + index_patterns: ['logs-*-*'], + }, + ], + }); }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts index 8e09e331bf867..85573560177ee 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts @@ -87,6 +87,40 @@ export default function (providerContext: FtrProviderContext) { ); expect(resMetricsTemplate.statusCode).equal(404); }); + it('should have uninstalled the component templates', async function () { + const resMappings = await es.transport.request( + { + method: 'GET', + path: `/_component_template/${logsTemplateName}@mappings`, + }, + { + ignore: [404], + } + ); + expect(resMappings.statusCode).equal(404); + + const resSettings = await es.transport.request( + { + method: 'GET', + path: `/_component_template/${logsTemplateName}@settings`, + }, + { + ignore: [404], + } + ); + expect(resSettings.statusCode).equal(404); + + const resUserSettings = await es.transport.request( + { + method: 'GET', + path: `/_component_template/${logsTemplateName}@custom`, + }, + { + ignore: [404], + } + ); + expect(resUserSettings.statusCode).equal(404); + }); it('should have uninstalled the pipelines', async function () { const res = await es.transport.request( { @@ -328,17 +362,22 @@ const expectAssetsInstalled = ({ }); expect(resPipeline2.statusCode).equal(200); }); - it('should have installed the template components', async function () { - const res = await es.transport.request({ + it('should have installed the component templates', async function () { + const resMappings = await es.transport.request({ method: 'GET', - path: `/_component_template/${logsTemplateName}-mappings`, + path: `/_component_template/${logsTemplateName}@mappings`, }); - expect(res.statusCode).equal(200); + expect(resMappings.statusCode).equal(200); const resSettings = await es.transport.request({ method: 'GET', - path: `/_component_template/${logsTemplateName}-settings`, + path: `/_component_template/${logsTemplateName}@settings`, }); expect(resSettings.statusCode).equal(200); + const resUserSettings = await es.transport.request({ + method: 'GET', + path: `/_component_template/${logsTemplateName}@custom`, + }); + expect(resUserSettings.statusCode).equal(200); }); it('should have installed the transform components', async function () { const res = await es.transport.request({ @@ -487,6 +526,22 @@ const expectAssetsInstalled = ({ }, ], installed_es: [ + { + id: 'logs-all_assets.test_logs@mappings', + type: 'component_template', + }, + { + id: 'logs-all_assets.test_logs@settings', + type: 'component_template', + }, + { + id: 'logs-all_assets.test_logs@custom', + type: 'component_template', + }, + { + id: 'metrics-all_assets.test_metrics@custom', + type: 'component_template', + }, { id: 'logs-all_assets.test_logs-all_assets', type: 'data_stream_ilm_policy', diff --git a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts index a6f79414ab8c0..6b4d104423144 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts @@ -199,23 +199,45 @@ export default function (providerContext: FtrProviderContext) { ); expect(resPipeline2.statusCode).equal(404); }); - it('should have updated the template components', async function () { - const res = await es.transport.request({ + it('should have updated the component templates', async function () { + const resMappings = await es.transport.request({ method: 'GET', - path: `/_component_template/${logsTemplateName}-mappings`, + path: `/_component_template/${logsTemplateName}@mappings`, }); - expect(res.statusCode).equal(200); - expect(res.body.component_templates[0].component_template.template.mappings).eql({ + expect(resMappings.statusCode).equal(200); + expect(resMappings.body.component_templates[0].component_template.template.mappings).eql({ dynamic: true, }); const resSettings = await es.transport.request({ method: 'GET', - path: `/_component_template/${logsTemplateName}-settings`, + path: `/_component_template/${logsTemplateName}@settings`, }); - expect(res.statusCode).equal(200); + expect(resSettings.statusCode).equal(200); expect(resSettings.body.component_templates[0].component_template.template.settings).eql({ index: { lifecycle: { name: 'reference2' } }, }); + const resUserSettings = await es.transport.request({ + method: 'GET', + path: `/_component_template/${logsTemplateName}@custom`, + }); + expect(resUserSettings.statusCode).equal(200); + expect(resUserSettings.body).eql({ + component_templates: [ + { + name: 'logs-all_assets.test_logs@custom', + component_template: { + _meta: { + package: { + name: 'all_assets', + }, + }, + template: { + settings: {}, + }, + }, + }, + ], + }); }); it('should have updated the index patterns', async function () { const resIndexPatternLogs = await kibanaServer.savedObjects.get({ @@ -321,14 +343,34 @@ export default function (providerContext: FtrProviderContext) { id: 'logs-all_assets.test_logs', type: 'index_template', }, + { + id: 'logs-all_assets.test_logs@mappings', + type: 'component_template', + }, + { + id: 'logs-all_assets.test_logs@settings', + type: 'component_template', + }, + { + id: 'logs-all_assets.test_logs@custom', + type: 'component_template', + }, { id: 'logs-all_assets.test_logs2', type: 'index_template', }, + { + id: 'logs-all_assets.test_logs2@custom', + type: 'component_template', + }, { id: 'metrics-all_assets.test_metrics', type: 'index_template', }, + { + id: 'metrics-all_assets.test_metrics@custom', + type: 'component_template', + }, ], es_index_patterns: { test_logs: 'logs-all_assets.test_logs-*', diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/img/logo_overrides_64_color.svg b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/img/logo_overrides_64_color.svg new file mode 100644 index 0000000000000..b03007a76ffcc --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/img/logo_overrides_64_color.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/manifest.yml index bba1a6a4c347d..312cd2874804c 100644 --- a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/manifest.yml +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/manifest.yml @@ -1,7 +1,7 @@ format_version: 1.0.0 -name: error_handling +name: error_handling title: Error handling -description: tests error handling and rollback +description: tests error handling and rollback version: 0.1.0 categories: [] release: beta @@ -17,4 +17,4 @@ requirement: icons: - src: '/img/logo_overrides_64_color.svg' size: '16x16' - type: 'image/svg+xml' \ No newline at end of file + type: 'image/svg+xml' diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/img/logo_overrides_64_color.svg b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/img/logo_overrides_64_color.svg new file mode 100644 index 0000000000000..b03007a76ffcc --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/img/logo_overrides_64_color.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/manifest.yml index 2eb6a41a77ede..c92f0ab5ae7f3 100644 --- a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/manifest.yml +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/manifest.yml @@ -1,7 +1,7 @@ format_version: 1.0.0 -name: error_handling +name: error_handling title: Error handling -description: tests error handling and rollback +description: tests error handling and rollback version: 0.2.0 categories: [] release: beta @@ -16,4 +16,4 @@ requirement: icons: - src: '/img/logo_overrides_64_color.svg' - size: '16x16' \ No newline at end of file + size: '16x16' diff --git a/x-pack/test/fleet_api_integration/config.ts b/x-pack/test/fleet_api_integration/config.ts index 52c9760d66c19..d18ba9c55ca96 100644 --- a/x-pack/test/fleet_api_integration/config.ts +++ b/x-pack/test/fleet_api_integration/config.ts @@ -51,17 +51,11 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { waitForLogLine: 'package manifests loaded', }, }), - services: { - ...xPackAPITestsConfig.get('services'), - }, + services: xPackAPITestsConfig.get('services'), junit: { reportName: 'X-Pack EPM API Integration Tests', }, - - esTestCluster: { - ...xPackAPITestsConfig.get('esTestCluster'), - }, - + esTestCluster: xPackAPITestsConfig.get('esTestCluster'), kbnTestServer: { ...xPackAPITestsConfig.get('kbnTestServer'), serverArgs: [ From 16f4abf90096234b490b020827b02a43e6a00bd2 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 14:36:07 -0400 Subject: [PATCH 26/43] Fixing the generator to use bulk api to install endpoint package (#103094) (#103127) Co-authored-by: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> --- .../endpoint/resolver_generator_script.ts | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts index 72f56f13eaddf..66ac744b3a50c 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts @@ -15,9 +15,16 @@ import { KbnClient } from '@kbn/test'; import { AxiosResponse } from 'axios'; import { indexHostsAndAlerts } from '../../common/endpoint/index_data'; import { ANCESTRY_LIMIT, EndpointDocGenerator } from '../../common/endpoint/generate_data'; -import { AGENTS_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../../fleet/common/constants'; import { + AGENTS_SETUP_API_ROUTES, + EPM_API_ROUTES, + SETUP_API_ROUTE, +} from '../../../fleet/common/constants'; +import { + BulkInstallPackageInfo, + BulkInstallPackagesResponse, CreateFleetSetupResponse, + IBulkInstallPackageHTTPError, PostIngestSetupResponse, } from '../../../fleet/common/types/rest_spec'; import { KbnClientWithApiKeySupport } from './kbn_client_with_api_key_support'; @@ -44,6 +51,12 @@ async function deleteIndices(indices: string[], client: Client) { } } +function isFleetBulkInstallError( + installResponse: BulkInstallPackageInfo | IBulkInstallPackageHTTPError +): installResponse is IBulkInstallPackageHTTPError { + return 'error' in installResponse && installResponse.error !== undefined; +} + async function doIngestSetup(kbnClient: KbnClient) { // Setup Ingest try { @@ -76,6 +89,35 @@ async function doIngestSetup(kbnClient: KbnClient) { console.error(error); throw error; } + + // Install/upgrade the endpoint package + try { + const installEndpointPackageResp = (await kbnClient.request({ + path: EPM_API_ROUTES.BULK_INSTALL_PATTERN, + method: 'POST', + body: { + packages: ['endpoint'], + }, + })) as AxiosResponse; + + const bulkResp = installEndpointPackageResp.data.response; + if (bulkResp.length <= 0) { + throw new Error('Installing the Endpoint package failed, response was empty, existing'); + } + + if (isFleetBulkInstallError(bulkResp[0])) { + if (bulkResp[0].error instanceof Error) { + throw new Error( + `Installing the Endpoint package failed: ${bulkResp[0].error.message}, exiting` + ); + } + + throw new Error(bulkResp[0].error); + } + } catch (error) { + console.error(error); + throw error; + } } async function main() { From 0d508191a0dfb1ee61148f8d5570eb25f9380f0f Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 14:38:23 -0400 Subject: [PATCH 27/43] Lens on Dashboard 7.12.1 Smoke Tests (#102667) (#103128) * added smoke tests for lens by value panels on dashboard Co-authored-by: Devon Thomson --- .../services/dashboard/panel_actions.ts | 60 +++++++------- .../test/functional/apps/dashboard/index.ts | 2 + ...ens_dashboard_migration_test_7_12_1.ndjson | 7 ++ .../lens_migration_smoke_test.ts | 83 +++++++++++++++++++ 4 files changed, 124 insertions(+), 28 deletions(-) create mode 100644 x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/lens_dashboard_migration_test_7_12_1.ndjson create mode 100644 x-pack/test/functional/apps/dashboard/migration_smoke_tests/lens_migration_smoke_test.ts diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index 9aca790b0b437..4340f16492a7c 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -211,36 +211,29 @@ export class DashboardPanelActionsService extends FtrService { await this.testSubjects.click('confirmSaveSavedObjectButton'); } - async expectExistsRemovePanelAction() { - this.log.debug('expectExistsRemovePanelAction'); - await this.expectExistsPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ); - } - - async expectExistsPanelAction(testSubject: string) { + async expectExistsPanelAction(testSubject: string, title?: string) { this.log.debug('expectExistsPanelAction', testSubject); - await this.openContextMenu(); - if (await this.testSubjects.exists(CLONE_PANEL_DATA_TEST_SUBJ)) return; - if (await this.hasContextMenuMoreItem()) { - await this.clickContextMenuMoreItem(); + + const panelWrapper = title ? await this.getPanelHeading(title) : undefined; + await this.openContextMenu(panelWrapper); + + if (!(await this.testSubjects.exists(testSubject))) { + if (await this.hasContextMenuMoreItem()) { + await this.clickContextMenuMoreItem(); + } + await this.testSubjects.existOrFail(testSubject); } - await this.testSubjects.existOrFail(CLONE_PANEL_DATA_TEST_SUBJ); - await this.toggleContextMenu(); + await this.toggleContextMenu(panelWrapper); } - async expectMissingPanelAction(testSubject: string) { - this.log.debug('expectMissingPanelAction', testSubject); - await this.openContextMenu(); - await this.testSubjects.missingOrFail(testSubject); - if (await this.hasContextMenuMoreItem()) { - await this.clickContextMenuMoreItem(); - await this.testSubjects.missingOrFail(testSubject); - } - await this.toggleContextMenu(); + async expectExistsRemovePanelAction() { + this.log.debug('expectExistsRemovePanelAction'); + await this.expectExistsPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ); } - async expectExistsEditPanelAction() { + async expectExistsEditPanelAction(title?: string) { this.log.debug('expectExistsEditPanelAction'); - await this.expectExistsPanelAction(EDIT_PANEL_DATA_TEST_SUBJ); + await this.expectExistsPanelAction(EDIT_PANEL_DATA_TEST_SUBJ, title); } async expectExistsReplacePanelAction() { @@ -253,6 +246,22 @@ export class DashboardPanelActionsService extends FtrService { await this.expectExistsPanelAction(CLONE_PANEL_DATA_TEST_SUBJ); } + async expectExistsToggleExpandAction() { + this.log.debug('expectExistsToggleExpandAction'); + await this.expectExistsPanelAction(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); + } + + async expectMissingPanelAction(testSubject: string) { + this.log.debug('expectMissingPanelAction', testSubject); + await this.openContextMenu(); + await this.testSubjects.missingOrFail(testSubject); + if (await this.hasContextMenuMoreItem()) { + await this.clickContextMenuMoreItem(); + await this.testSubjects.missingOrFail(testSubject); + } + await this.toggleContextMenu(); + } + async expectMissingEditPanelAction() { this.log.debug('expectMissingEditPanelAction'); await this.expectMissingPanelAction(EDIT_PANEL_DATA_TEST_SUBJ); @@ -273,11 +282,6 @@ export class DashboardPanelActionsService extends FtrService { await this.expectMissingPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ); } - async expectExistsToggleExpandAction() { - this.log.debug('expectExistsToggleExpandAction'); - await this.expectExistsPanelAction(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); - } - async getPanelHeading(title: string) { return await this.testSubjects.find(`embeddablePanelHeading-${title.replace(/\s/g, '')}`); } diff --git a/x-pack/test/functional/apps/dashboard/index.ts b/x-pack/test/functional/apps/dashboard/index.ts index 1d046c7c18218..99f8c6ffedefc 100644 --- a/x-pack/test/functional/apps/dashboard/index.ts +++ b/x-pack/test/functional/apps/dashboard/index.ts @@ -19,5 +19,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_async_dashboard')); loadTestFile(require.resolve('./dashboard_lens_by_value')); loadTestFile(require.resolve('./dashboard_maps_by_value')); + + loadTestFile(require.resolve('./migration_smoke_tests/lens_migration_smoke_test')); }); } diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/lens_dashboard_migration_test_7_12_1.ndjson b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/lens_dashboard_migration_test_7_12_1.ndjson new file mode 100644 index 0000000000000..cdf6e94537ae6 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/lens_dashboard_migration_test_7_12_1.ndjson @@ -0,0 +1,7 @@ +{"attributes":{"fieldAttrs":"{}","fields":"[]","runtimeFieldMap":"{}","title":"shakespeare"},"coreMigrationVersion":"7.12.2","id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","migrationVersion":{"index-pattern":"7.11.0"},"references":[],"type":"index-pattern","updated_at":"2021-06-17T22:28:02.495Z","version":"WzEyLDJd"} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"},"optionsJSON":"{\"useMargins\":true,\"hidePanelTitles\":false}","panelsJSON":"[]","timeRestore":false,"title":"Blank Destination Dashboard","version":1},"coreMigrationVersion":"7.12.2","id":"759faf20-cfbd-11eb-984d-af3b44ed60a7","migrationVersion":{"dashboard":"7.11.0"},"references":[],"type":"dashboard","updated_at":"2021-06-17T22:43:39.414Z","version":"WzI1MiwyXQ=="} +{"attributes":{"state":{"datasourceStates":{"indexpattern":{"layers":{"8faa1a43-2c03-4277-b19b-575da8b59561":{"columnOrder":["20d61a13-4000-4df2-9d83-d9ec0c87b32a","6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe"],"columns":{"20d61a13-4000-4df2-9d83-d9ec0c87b32a":{"dataType":"string","isBucketed":true,"label":"Top values of speaker","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe","type":"column"},"orderDirection":"desc","otherBucket":true,"size":20},"scale":"ordinal","sourceField":"speaker"},"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}},"incompleteColumns":{}}}}},"filters":[{"$state":{"store":"appState"},"meta":{"alias":null,"disabled":false,"indexRefName":"filter-index-pattern-0","key":"play_name","negate":false,"params":{"query":"Hamlet"},"type":"phrase"},"query":{"match_phrase":{"play_name":"Hamlet"}}},{"$state":{"store":"appState"},"meta":{"alias":null,"disabled":false,"indexRefName":"filter-index-pattern-1","key":"speaker","negate":true,"params":{"query":"HAMLET"},"type":"phrase"},"query":{"match_phrase":{"speaker":"HAMLET"}}}],"query":{"language":"kuery","query":""},"visualization":{"layers":[{"categoryDisplay":"default","groups":["20d61a13-4000-4df2-9d83-d9ec0c87b32a","20d61a13-4000-4df2-9d83-d9ec0c87b32a","20d61a13-4000-4df2-9d83-d9ec0c87b32a","20d61a13-4000-4df2-9d83-d9ec0c87b32a","20d61a13-4000-4df2-9d83-d9ec0c87b32a","20d61a13-4000-4df2-9d83-d9ec0c87b32a","20d61a13-4000-4df2-9d83-d9ec0c87b32a"],"layerId":"8faa1a43-2c03-4277-b19b-575da8b59561","legendDisplay":"default","metric":"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe","nestedLegend":false,"numberDisplay":"percent"}],"shape":"donut"}},"title":"Lens by Reference With Various Filters","visualizationType":"lnsPie"},"coreMigrationVersion":"7.12.2","id":"bf5d7860-cfbb-11eb-984d-af3b44ed60a7","migrationVersion":{"lens":"7.12.0"},"references":[{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-layer-8faa1a43-2c03-4277-b19b-575da8b59561","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"filter-index-pattern-0","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"filter-index-pattern-1","type":"index-pattern"}],"type":"lens","updated_at":"2021-06-17T22:31:24.138Z","version":"WzgzLDJd"} +{"attributes":{"state":{"datasourceStates":{"indexpattern":{"layers":{"b349d4ba-df44-415f-b0be-999b12f52213":{"columnOrder":["73bc446b-31e8-47a1-b7a1-9549bc81570a","45d911a5-6178-4d9a-a8b4-702a8377c859","89abee74-0f49-4e13-b0e1-2698af72c6f6"],"columns":{"45d911a5-6178-4d9a-a8b4-702a8377c859":{"dataType":"string","isBucketed":true,"label":"Top values of speaker","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"89abee74-0f49-4e13-b0e1-2698af72c6f6","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"speaker"},"73bc446b-31e8-47a1-b7a1-9549bc81570a":{"dataType":"string","isBucketed":true,"label":"Top values of play_name","operationType":"terms","params":{"missingBucket":false,"orderBy":{"type":"alphabetical"},"orderDirection":"asc","otherBucket":true,"size":5},"scale":"ordinal","sourceField":"play_name"},"89abee74-0f49-4e13-b0e1-2698af72c6f6":{"dataType":"number","isBucketed":false,"label":"Average of speech_number","operationType":"avg","scale":"ratio","sourceField":"speech_number"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["89abee74-0f49-4e13-b0e1-2698af72c6f6"],"layerId":"b349d4ba-df44-415f-b0be-999b12f52213","position":"top","seriesType":"bar_stacked","showGridlines":false,"splitAccessor":"45d911a5-6178-4d9a-a8b4-702a8377c859","xAccessor":"73bc446b-31e8-47a1-b7a1-9549bc81570a"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"[7.12.1] Lens By Reference with Average","visualizationType":"lnsXY"},"coreMigrationVersion":"7.12.2","id":"09ae9610-cfbc-11eb-984d-af3b44ed60a7","migrationVersion":{"lens":"7.12.0"},"references":[{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-layer-b349d4ba-df44-415f-b0be-999b12f52213","type":"index-pattern"}],"type":"lens","updated_at":"2021-06-17T22:33:28.820Z","version":"WzEyMywyXQ=="} +{"attributes":{"state":{"datasourceStates":{"indexpattern":{"layers":{"7d197461-9572-4437-b565-f3d7ec731753":{"columnOrder":["de0485bc-e55f-45d1-bf14-2a252ff718d0","a8fced94-076e-44ac-9e94-c0e3847e51b5"],"columns":{"a8fced94-076e-44ac-9e94-c0e3847e51b5":{"dataType":"number","isBucketed":false,"label":"Average of speech_number","operationType":"avg","scale":"ratio","sourceField":"speech_number"},"de0485bc-e55f-45d1-bf14-2a252ff718d0":{"dataType":"string","isBucketed":true,"label":"Top values of type.keyword","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"a8fced94-076e-44ac-9e94-c0e3847e51b5","type":"column"},"orderDirection":"desc","otherBucket":true,"size":5},"scale":"ordinal","sourceField":"type.keyword"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["a8fced94-076e-44ac-9e94-c0e3847e51b5"],"layerId":"7d197461-9572-4437-b565-f3d7ec731753","seriesType":"bar_stacked","xAccessor":"de0485bc-e55f-45d1-bf14-2a252ff718d0"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"[7.12.1] Lens By Reference with Drilldown","visualizationType":"lnsXY"},"coreMigrationVersion":"7.12.2","id":"8ac83fc0-cfbd-11eb-984d-af3b44ed60a7","migrationVersion":{"lens":"7.12.0"},"references":[{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-layer-7d197461-9572-4437-b565-f3d7ec731753","type":"index-pattern"}],"type":"lens","updated_at":"2021-06-17T22:44:14.911Z","version":"WzI2OSwyXQ=="} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.12.2\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":9,\"h\":15,\"i\":\"64bf6149-4bba-423e-91b4-9ff160f520e0\"},\"panelIndex\":\"64bf6149-4bba-423e-91b4-9ff160f520e0\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsPie\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"8faa1a43-2c03-4277-b19b-575da8b59561\":{\"columns\":{\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\":{\"label\":\"Top values of speaker\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"speaker\",\"isBucketed\":true,\"params\":{\"size\":20,\"orderBy\":{\"type\":\"column\",\"columnId\":\"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"Records\"}},\"columnOrder\":[\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"shape\":\"donut\",\"layers\":[{\"layerId\":\"8faa1a43-2c03-4277-b19b-575da8b59561\",\"groups\":[\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\"],\"metric\":\"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe\",\"numberDisplay\":\"percent\",\"categoryDisplay\":\"default\",\"legendDisplay\":\"default\",\"nestedLegend\":false}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"alias\":null,\"negate\":false,\"disabled\":false,\"type\":\"phrase\",\"key\":\"play_name\",\"params\":{\"query\":\"Hamlet\"},\"indexRefName\":\"filter-index-pattern-0\"},\"query\":{\"match_phrase\":{\"play_name\":\"Hamlet\"}},\"$state\":{\"store\":\"appState\"}},{\"meta\":{\"alias\":null,\"negate\":true,\"disabled\":false,\"type\":\"phrase\",\"key\":\"speaker\",\"params\":{\"query\":\"HAMLET\"},\"indexRefName\":\"filter-index-pattern-1\"},\"query\":{\"match_phrase\":{\"speaker\":\"HAMLET\"}},\"$state\":{\"store\":\"appState\"}}]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\",\"name\":\"indexpattern-datasource-layer-8faa1a43-2c03-4277-b19b-575da8b59561\"},{\"name\":\"filter-index-pattern-0\",\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\"},{\"name\":\"filter-index-pattern-1\",\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\"}]},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"[7.12.1] Lens by Value With Various Filters\"},{\"version\":\"7.12.2\",\"type\":\"lens\",\"gridData\":{\"x\":9,\"y\":0,\"w\":24,\"h\":15,\"i\":\"6ecd96ef-70cc-4d80-a5e5-a2d5b43a2236\"},\"panelIndex\":\"6ecd96ef-70cc-4d80-a5e5-a2d5b43a2236\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"b349d4ba-df44-415f-b0be-999b12f52213\":{\"columns\":{\"73bc446b-31e8-47a1-b7a1-9549bc81570a\":{\"label\":\"Top values of play_name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"play_name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"alphabetical\"},\"orderDirection\":\"asc\",\"otherBucket\":true,\"missingBucket\":false}},\"89abee74-0f49-4e13-b0e1-2698af72c6f6\":{\"label\":\"Average of speech_number\",\"dataType\":\"number\",\"operationType\":\"avg\",\"sourceField\":\"speech_number\",\"isBucketed\":false,\"scale\":\"ratio\"},\"45d911a5-6178-4d9a-a8b4-702a8377c859\":{\"label\":\"Top values of speaker\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"speaker\",\"isBucketed\":true,\"params\":{\"size\":3,\"orderBy\":{\"type\":\"column\",\"columnId\":\"89abee74-0f49-4e13-b0e1-2698af72c6f6\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}}},\"columnOrder\":[\"73bc446b-31e8-47a1-b7a1-9549bc81570a\",\"45d911a5-6178-4d9a-a8b4-702a8377c859\",\"89abee74-0f49-4e13-b0e1-2698af72c6f6\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"b349d4ba-df44-415f-b0be-999b12f52213\",\"accessors\":[\"89abee74-0f49-4e13-b0e1-2698af72c6f6\"],\"position\":\"top\",\"seriesType\":\"bar_stacked\",\"showGridlines\":false,\"xAccessor\":\"73bc446b-31e8-47a1-b7a1-9549bc81570a\",\"splitAccessor\":\"45d911a5-6178-4d9a-a8b4-702a8377c859\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\",\"name\":\"indexpattern-datasource-layer-b349d4ba-df44-415f-b0be-999b12f52213\"}]},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"[7.12.1] Lens By Value Lens with Average\"},{\"version\":\"7.12.2\",\"type\":\"lens\",\"gridData\":{\"x\":33,\"y\":0,\"w\":15,\"h\":15,\"i\":\"fed39777-b755-45f8-9efb-1203b4b3d7cf\"},\"panelIndex\":\"fed39777-b755-45f8-9efb-1203b4b3d7cf\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"7d197461-9572-4437-b565-f3d7ec731753\":{\"columns\":{\"de0485bc-e55f-45d1-bf14-2a252ff718d0\":{\"label\":\"Top values of type.keyword\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"type.keyword\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"a8fced94-076e-44ac-9e94-c0e3847e51b5\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"a8fced94-076e-44ac-9e94-c0e3847e51b5\":{\"label\":\"Average of speech_number\",\"dataType\":\"number\",\"operationType\":\"avg\",\"sourceField\":\"speech_number\",\"isBucketed\":false,\"scale\":\"ratio\"}},\"columnOrder\":[\"de0485bc-e55f-45d1-bf14-2a252ff718d0\",\"a8fced94-076e-44ac-9e94-c0e3847e51b5\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"7d197461-9572-4437-b565-f3d7ec731753\",\"seriesType\":\"bar_stacked\",\"accessors\":[\"a8fced94-076e-44ac-9e94-c0e3847e51b5\"],\"xAccessor\":\"de0485bc-e55f-45d1-bf14-2a252ff718d0\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\",\"name\":\"indexpattern-datasource-layer-7d197461-9572-4437-b565-f3d7ec731753\"}]},\"hidePanelTitles\":false,\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"8934b2f2-b989-4b8c-8339-c95e387f4372\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"Test Drilldown\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}}},\"title\":\"[7.12.1] Lens By Value with Drilldown\"},{\"version\":\"7.12.2\",\"gridData\":{\"x\":0,\"y\":15,\"w\":9,\"h\":15,\"i\":\"eb826c7a-0ead-4c8d-99cf-d823388bb91d\"},\"panelIndex\":\"eb826c7a-0ead-4c8d-99cf-d823388bb91d\",\"embeddableConfig\":{\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"[7.12.1] Lens by Reference With Various Filters\",\"panelRefName\":\"panel_3\"},{\"version\":\"7.12.2\",\"gridData\":{\"x\":9,\"y\":15,\"w\":24,\"h\":15,\"i\":\"80a4927b-aa69-4c80-ad38-482c141d0b93\"},\"panelIndex\":\"80a4927b-aa69-4c80-ad38-482c141d0b93\",\"embeddableConfig\":{\"hidePanelTitles\":false,\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"7.12.2\",\"gridData\":{\"x\":33,\"y\":15,\"w\":15,\"h\":15,\"i\":\"57a79145-1314-49f3-87e3-7c494cf55f64\"},\"panelIndex\":\"57a79145-1314-49f3-87e3-7c494cf55f64\",\"embeddableConfig\":{\"hidePanelTitles\":false,\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"8934b2f2-b989-4b8c-8339-c95e387f4372\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"Test Drilldown\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}}},\"panelRefName\":\"panel_5\"}]","timeRestore":false,"title":"[7.12.1] Lens By Value Test Dashboard","version":1},"coreMigrationVersion":"7.12.2","id":"60a5cfa0-cfbd-11eb-984d-af3b44ed60a7","migrationVersion":{"dashboard":"7.11.0"},"references":[{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-layer-8faa1a43-2c03-4277-b19b-575da8b59561","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"filter-index-pattern-0","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"filter-index-pattern-1","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-layer-b349d4ba-df44-415f-b0be-999b12f52213","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-layer-7d197461-9572-4437-b565-f3d7ec731753","type":"index-pattern"},{"id":"759faf20-cfbd-11eb-984d-af3b44ed60a7","name":"drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:8934b2f2-b989-4b8c-8339-c95e387f4372:dashboardId","type":"dashboard"},{"id":"759faf20-cfbd-11eb-984d-af3b44ed60a7","name":"drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:8934b2f2-b989-4b8c-8339-c95e387f4372:dashboardId","type":"dashboard"},{"id":"bf5d7860-cfbb-11eb-984d-af3b44ed60a7","name":"panel_3","type":"lens"},{"id":"09ae9610-cfbc-11eb-984d-af3b44ed60a7","name":"panel_4","type":"lens"},{"id":"8ac83fc0-cfbd-11eb-984d-af3b44ed60a7","name":"panel_5","type":"lens"}],"type":"dashboard","updated_at":"2021-06-17T22:44:36.881Z","version":"WzI3NSwyXQ=="} +{"exportedCount":6,"missingRefCount":0,"missingReferences":[]} \ No newline at end of file diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/lens_migration_smoke_test.ts b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/lens_migration_smoke_test.ts new file mode 100644 index 0000000000000..78b7ccfe7df08 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/lens_migration_smoke_test.ts @@ -0,0 +1,83 @@ +/* + * Copyright 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. + */ + +/* This test is importing saved objects from 7.13.0 to 8.0 and the backported version + * will import from 6.8.x to 7.x.x + */ + +import expect from '@kbn/expect'; +import path from 'path'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + const dashboardPanelActions = getService('dashboardPanelActions'); + + const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects', 'dashboard']); + + describe('Export import saved objects between versions', () => { + before(async () => { + await esArchiver.loadIfNeeded( + 'x-pack/test/functional/es_archives/getting_started/shakespeare' + ); + await kibanaServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); + await PageObjects.savedObjects.importFile( + path.join(__dirname, 'exports', 'lens_dashboard_migration_test_7_12_1.ndjson') + ); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/getting_started/shakespeare'); + await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + }); + + it('should be able to import dashboard with various Lens panels from 7.12.1', async () => { + // this will catch cases where there is an error in the migrations. + await PageObjects.savedObjects.checkImportSucceeded(); + await PageObjects.savedObjects.clickImportDone(); + }); + + it('should render all panels on the dashboard', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('[7.12.1] Lens By Value Test Dashboard'); + + // dashboard should load properly + await PageObjects.dashboard.expectOnDashboard('[7.12.1] Lens By Value Test Dashboard'); + await PageObjects.dashboard.waitForRenderComplete(); + + // There should be 0 error embeddables on the dashboard + const errorEmbeddables = await testSubjects.findAll('embeddableStackError'); + expect(errorEmbeddables.length).to.be(0); + }); + + it('should show the edit action for all panels', async () => { + await PageObjects.dashboard.switchToEditMode(); + + // All panels should be editable. This will catch cases where an error does not create an error embeddable. + const panelTitles = await PageObjects.dashboard.getPanelTitles(); + for (const title of panelTitles) { + await dashboardPanelActions.expectExistsEditPanelAction(title); + } + }); + + it('should retain all panel drilldowns from 7.12.1', async () => { + // Both panels configured with drilldowns in 7.12.1 should still have drilldowns. + const totalPanels = await PageObjects.dashboard.getPanelCount(); + let panelsWithDrilldowns = 0; + for (let panelIndex = 0; panelIndex < totalPanels; panelIndex++) { + if ((await PageObjects.dashboard.getPanelDrilldownCount(panelIndex)) === 1) { + panelsWithDrilldowns++; + } + } + expect(panelsWithDrilldowns).to.be(2); + }); + }); +} From d457f3b2f7731a8f32d8e8c07470ab7e1f8bdc51 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 23 Jun 2021 12:51:17 -0600 Subject: [PATCH 28/43] merge with master (#103132) --- .../data_request_descriptor_types.ts | 4 + .../blended_vector_layer.ts | 34 ++---- .../layers/heatmap_layer/heatmap_layer.ts | 3 + .../maps/public/classes/layers/layer.tsx | 3 +- .../tiled_vector_layer/tiled_vector_layer.tsx | 7 +- .../classes/layers/vector_layer/utils.tsx | 20 ++- .../layers/vector_layer/vector_layer.tsx | 100 +++++++++++---- .../es_search_source/es_search_source.tsx | 105 ++++++++++++++-- .../mvt_single_layer_vector_source.tsx | 4 + .../maps/public/classes/sources/source.ts | 12 +- .../sources/vector_source/vector_source.tsx | 5 + .../classes/util/can_skip_fetch.test.js | 21 ++++ .../public/classes/util/can_skip_fetch.ts | 8 +- .../classes/util/mb_filter_expressions.ts | 115 ++++++++++++------ .../connected_components/mb_map/index.ts | 2 + .../connected_components/mb_map/mb_map.tsx | 49 +++----- .../maps/documents_source/docvalue_fields.js | 11 +- 17 files changed, 369 insertions(+), 134 deletions(-) diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts index 07de57d0ac832..d1690ddfff43d 100644 --- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts @@ -66,6 +66,7 @@ export type VectorSourceRequestMeta = MapFilters & { applyGlobalTime: boolean; fieldNames: string[]; geogridPrecision?: number; + timesiceMaskField?: string; sourceQuery?: MapQuery; sourceMeta: VectorSourceSyncMeta; }; @@ -84,6 +85,9 @@ export type VectorStyleRequestMeta = MapFilters & { export type ESSearchSourceResponseMeta = { areResultsTrimmed?: boolean; resultsCount?: number; + // results time extent, either Kibana time range or timeslider time slice + timeExtent?: Timeslice; + isTimeExtentForTimeslice?: boolean; // top hits meta areEntitiesTrimmed?: boolean; diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts index 6dd454137be7d..9bfa74825c338 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -22,7 +22,6 @@ import { LAYER_STYLE_TYPE, FIELD_ORIGIN, } from '../../../../common/constants'; -import { isTotalHitsGreaterThan, TotalHits } from '../../../../common/elasticsearch_util'; import { ESGeoGridSource } from '../../sources/es_geo_grid_source/es_geo_grid_source'; import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; import { IESSource } from '../../sources/es_source'; @@ -35,6 +34,7 @@ import { DynamicStylePropertyOptions, StylePropertyOptions, LayerDescriptor, + Timeslice, VectorLayerDescriptor, VectorSourceRequestMeta, VectorStylePropertiesDescriptor, @@ -46,10 +46,6 @@ import { isSearchSourceAbortError } from '../../sources/es_source/es_source'; const ACTIVE_COUNT_DATA_ID = 'ACTIVE_COUNT_DATA_ID'; -interface CountData { - isSyncClustered: boolean; -} - function getAggType( dynamicProperty: IDynamicStyleProperty ): AGG_TYPE.AVG | AGG_TYPE.TERMS { @@ -216,7 +212,7 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { let isClustered = false; const countDataRequest = this.getDataRequest(ACTIVE_COUNT_DATA_ID); if (countDataRequest) { - const requestData = countDataRequest.getData() as CountData; + const requestData = countDataRequest.getData() as { isSyncClustered: boolean }; if (requestData && requestData.isSyncClustered) { isClustered = true; } @@ -294,7 +290,7 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { async syncData(syncContext: DataRequestContext) { const dataRequestId = ACTIVE_COUNT_DATA_ID; const requestToken = Symbol(`layer-active-count:${this.getId()}`); - const searchFilters: VectorSourceRequestMeta = this._getSearchFilters( + const searchFilters: VectorSourceRequestMeta = await this._getSearchFilters( syncContext.dataFilters, this.getSource(), this.getCurrentStyle() @@ -305,6 +301,9 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { prevDataRequest: this.getDataRequest(dataRequestId), nextMeta: searchFilters, extentAware: source.isFilterByMapBounds(), + getUpdateDueToTimeslice: (timeslice?: Timeslice) => { + return this._getUpdateDueToTimesliceFromSourceRequestMeta(source, timeslice); + }, }); let activeSource; @@ -322,22 +321,11 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { let isSyncClustered; try { syncContext.startLoading(dataRequestId, requestToken, searchFilters); - const abortController = new AbortController(); - syncContext.registerCancelCallback(requestToken, () => abortController.abort()); - const maxResultWindow = await this._documentSource.getMaxResultWindow(); - const searchSource = await this._documentSource.makeSearchSource(searchFilters, 0); - searchSource.setField('trackTotalHits', maxResultWindow + 1); - const resp = await searchSource.fetch({ - abortSignal: abortController.signal, - sessionId: syncContext.dataFilters.searchSessionId, - legacyHitsTotal: false, - }); - isSyncClustered = isTotalHitsGreaterThan( - (resp.hits.total as unknown) as TotalHits, - maxResultWindow - ); - const countData = { isSyncClustered } as CountData; - syncContext.stopLoading(dataRequestId, requestToken, countData, searchFilters); + isSyncClustered = !(await this._documentSource.canLoadAllDocuments( + searchFilters, + syncContext.registerCancelCallback.bind(null, requestToken) + )); + syncContext.stopLoading(dataRequestId, requestToken, { isSyncClustered }, searchFilters); } catch (error) { if (!(error instanceof DataRequestAbortError) || !isSearchSourceAbortError(error)) { syncContext.onLoadError(dataRequestId, requestToken, error.message); diff --git a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts index 368ff8bebcdd1..d12c8432a4191 100644 --- a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts @@ -111,6 +111,9 @@ export class HeatmapLayer extends AbstractLayer { }, syncContext, source: this.getSource(), + getUpdateDueToTimeslice: () => { + return true; + }, }); } catch (error) { if (!(error instanceof DataRequestAbortError)) { diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index be113ab4cc2c9..ef41c157a2b17 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -36,6 +36,7 @@ import { LayerDescriptor, MapExtent, StyleDescriptor, + Timeslice, } from '../../../common/descriptor_types'; import { ImmutableSourceProperty, ISource, SourceEditorArgs } from '../sources/source'; import { DataRequestContext } from '../../actions'; @@ -78,7 +79,7 @@ export interface ILayer { getMbLayerIds(): string[]; ownsMbLayerId(mbLayerId: string): boolean; ownsMbSourceId(mbSourceId: string): boolean; - syncLayerWithMB(mbMap: MbMap): void; + syncLayerWithMB(mbMap: MbMap, timeslice?: Timeslice): void; getLayerTypeIconName(): string; isInitialDataLoadComplete(): boolean; getIndexPatternIds(): string[]; diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx index 6dba935ccc87d..2ad6a5ef73c6d 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx @@ -21,6 +21,7 @@ import { VectorLayer, VectorLayerArguments } from '../vector_layer'; import { ITiledSingleLayerVectorSource } from '../../sources/tiled_single_layer_vector_source'; import { DataRequestContext } from '../../../actions'; import { + Timeslice, VectorLayerDescriptor, VectorSourceRequestMeta, } from '../../../../common/descriptor_types'; @@ -66,7 +67,7 @@ export class TiledVectorLayer extends VectorLayer { dataFilters, }: DataRequestContext) { const requestToken: symbol = Symbol(`layer-${this.getId()}-${SOURCE_DATA_REQUEST_ID}`); - const searchFilters: VectorSourceRequestMeta = this._getSearchFilters( + const searchFilters: VectorSourceRequestMeta = await this._getSearchFilters( dataFilters, this.getSource(), this._style as IVectorStyle @@ -84,6 +85,10 @@ export class TiledVectorLayer extends VectorLayer { source: this.getSource(), prevDataRequest, nextMeta: searchFilters, + getUpdateDueToTimeslice: (timeslice?: Timeslice) => { + // TODO use meta features to determine if tiles already contain features for timeslice. + return true; + }, }); const canSkip = noChangesInSourceState && noChangesInSearchState; if (canSkip) { diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx index d305bb920b2ad..346e59f60af32 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx @@ -13,7 +13,13 @@ import { SOURCE_DATA_REQUEST_ID, VECTOR_SHAPE_TYPE, } from '../../../../common/constants'; -import { MapExtent, MapQuery, VectorSourceRequestMeta } from '../../../../common/descriptor_types'; +import { + DataMeta, + MapExtent, + MapQuery, + Timeslice, + VectorSourceRequestMeta, +} from '../../../../common/descriptor_types'; import { DataRequestContext } from '../../../actions'; import { IVectorSource } from '../../sources/vector_source'; import { DataRequestAbortError } from '../../util/data_request'; @@ -52,6 +58,7 @@ export async function syncVectorSource({ requestMeta, syncContext, source, + getUpdateDueToTimeslice, }: { layerId: string; layerName: string; @@ -59,6 +66,7 @@ export async function syncVectorSource({ requestMeta: VectorSourceRequestMeta; syncContext: DataRequestContext; source: IVectorSource; + getUpdateDueToTimeslice: (timeslice?: Timeslice) => boolean; }): Promise<{ refreshed: boolean; featureCollection: FeatureCollection }> { const { startLoading, @@ -76,6 +84,7 @@ export async function syncVectorSource({ prevDataRequest, nextMeta: requestMeta, extentAware: source.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); if (canSkipFetch) { return { @@ -104,7 +113,14 @@ export async function syncVectorSource({ ) { layerFeatureCollection.features.push(...getCentroidFeatures(layerFeatureCollection)); } - stopLoading(dataRequestId, requestToken, layerFeatureCollection, meta); + const responseMeta: DataMeta = meta ? { ...meta } : {}; + if (requestMeta.applyGlobalTime && (await source.isTimeAware())) { + const timesiceMaskField = await source.getTimesliceMaskFieldName(); + if (timesiceMaskField) { + responseMeta.timesiceMaskField = timesiceMaskField; + } + } + stopLoading(dataRequestId, requestToken, layerFeatureCollection, responseMeta); return { refreshed: true, featureCollection: layerFeatureCollection, diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index 8b4d25f4612cc..49a0878ef80b2 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -43,16 +43,19 @@ import { getFillFilterExpression, getLineFilterExpression, getPointFilterExpression, + TimesliceMaskConfig, } from '../../util/mb_filter_expressions'; import { DynamicStylePropertyOptions, MapFilters, MapQuery, + Timeslice, VectorJoinSourceRequestMeta, VectorLayerDescriptor, VectorSourceRequestMeta, VectorStyleRequestMeta, } from '../../../../common/descriptor_types'; +import { ISource } from '../../sources/source'; import { IVectorSource } from '../../sources/vector_source'; import { CustomIconAndTooltipContent, ILayer } from '../layer'; import { InnerJoin } from '../../joins/inner_join'; @@ -347,6 +350,9 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { prevDataRequest, nextMeta: searchFilters, extentAware: false, // join-sources are term-aggs that are spatially unaware (e.g. ESTermSource/TableSource). + getUpdateDueToTimeslice: () => { + return true; + }, }); if (canSkipFetch) { return { @@ -389,17 +395,22 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { return await Promise.all(joinSyncs); } - _getSearchFilters( + async _getSearchFilters( dataFilters: MapFilters, source: IVectorSource, style: IVectorStyle - ): VectorSourceRequestMeta { + ): Promise { const fieldNames = [ ...source.getFieldNames(), ...style.getSourceFieldNames(), ...this.getValidJoins().map((join) => join.getLeftField().getName()), ]; + const timesliceMaskFieldName = await source.getTimesliceMaskFieldName(); + if (timesliceMaskFieldName) { + fieldNames.push(timesliceMaskFieldName); + } + const sourceQuery = this.getQuery() as MapQuery; return { ...dataFilters, @@ -674,9 +685,12 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { layerId: this.getId(), layerName: await this.getDisplayName(source), prevDataRequest: this.getSourceDataRequest(), - requestMeta: this._getSearchFilters(syncContext.dataFilters, source, style), + requestMeta: await this._getSearchFilters(syncContext.dataFilters, source, style), syncContext, source, + getUpdateDueToTimeslice: (timeslice?: Timeslice) => { + return this._getUpdateDueToTimesliceFromSourceRequestMeta(source, timeslice); + }, }); await this._syncSupportsFeatureEditing({ syncContext, source }); if ( @@ -754,7 +768,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } } - _setMbPointsProperties(mbMap: MbMap, mvtSourceLayer?: string) { + _setMbPointsProperties( + mbMap: MbMap, + mvtSourceLayer?: string, + timesliceMaskConfig?: TimesliceMaskConfig + ) { const pointLayerId = this._getMbPointLayerId(); const symbolLayerId = this._getMbSymbolLayerId(); const pointLayer = mbMap.getLayer(pointLayerId); @@ -771,7 +789,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { if (symbolLayer) { mbMap.setLayoutProperty(symbolLayerId, 'visibility', 'none'); } - this._setMbCircleProperties(mbMap, mvtSourceLayer); + this._setMbCircleProperties(mbMap, mvtSourceLayer, timesliceMaskConfig); } else { markerLayerId = symbolLayerId; textLayerId = symbolLayerId; @@ -779,7 +797,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.setLayoutProperty(pointLayerId, 'visibility', 'none'); mbMap.setLayoutProperty(this._getMbTextLayerId(), 'visibility', 'none'); } - this._setMbSymbolProperties(mbMap, mvtSourceLayer); + this._setMbSymbolProperties(mbMap, mvtSourceLayer, timesliceMaskConfig); } this.syncVisibilityWithMb(mbMap, markerLayerId); @@ -790,7 +808,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } } - _setMbCircleProperties(mbMap: MbMap, mvtSourceLayer?: string) { + _setMbCircleProperties( + mbMap: MbMap, + mvtSourceLayer?: string, + timesliceMaskConfig?: TimesliceMaskConfig + ) { const sourceId = this.getId(); const pointLayerId = this._getMbPointLayerId(); const pointLayer = mbMap.getLayer(pointLayerId); @@ -822,7 +844,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.addLayer(mbLayer); } - const filterExpr = getPointFilterExpression(this.hasJoins()); + const filterExpr = getPointFilterExpression(this.hasJoins(), timesliceMaskConfig); if (!_.isEqual(filterExpr, mbMap.getFilter(pointLayerId))) { mbMap.setFilter(pointLayerId, filterExpr); mbMap.setFilter(textLayerId, filterExpr); @@ -841,7 +863,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { }); } - _setMbSymbolProperties(mbMap: MbMap, mvtSourceLayer?: string) { + _setMbSymbolProperties( + mbMap: MbMap, + mvtSourceLayer?: string, + timesliceMaskConfig?: TimesliceMaskConfig + ) { const sourceId = this.getId(); const symbolLayerId = this._getMbSymbolLayerId(); const symbolLayer = mbMap.getLayer(symbolLayerId); @@ -858,7 +884,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.addLayer(mbLayer); } - const filterExpr = getPointFilterExpression(this.hasJoins()); + const filterExpr = getPointFilterExpression(this.hasJoins(), timesliceMaskConfig); if (!_.isEqual(filterExpr, mbMap.getFilter(symbolLayerId))) { mbMap.setFilter(symbolLayerId, filterExpr); } @@ -876,7 +902,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { }); } - _setMbLinePolygonProperties(mbMap: MbMap, mvtSourceLayer?: string) { + _setMbLinePolygonProperties( + mbMap: MbMap, + mvtSourceLayer?: string, + timesliceMaskConfig?: TimesliceMaskConfig + ) { const sourceId = this.getId(); const fillLayerId = this._getMbPolygonLayerId(); const lineLayerId = this._getMbLineLayerId(); @@ -940,14 +970,14 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { this.syncVisibilityWithMb(mbMap, fillLayerId); mbMap.setLayerZoomRange(fillLayerId, this.getMinZoom(), this.getMaxZoom()); - const fillFilterExpr = getFillFilterExpression(hasJoins); + const fillFilterExpr = getFillFilterExpression(hasJoins, timesliceMaskConfig); if (!_.isEqual(fillFilterExpr, mbMap.getFilter(fillLayerId))) { mbMap.setFilter(fillLayerId, fillFilterExpr); } this.syncVisibilityWithMb(mbMap, lineLayerId); mbMap.setLayerZoomRange(lineLayerId, this.getMinZoom(), this.getMaxZoom()); - const lineFilterExpr = getLineFilterExpression(hasJoins); + const lineFilterExpr = getLineFilterExpression(hasJoins, timesliceMaskConfig); if (!_.isEqual(lineFilterExpr, mbMap.getFilter(lineLayerId))) { mbMap.setFilter(lineLayerId, lineFilterExpr); } @@ -956,7 +986,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.setLayerZoomRange(tooManyFeaturesLayerId, this.getMinZoom(), this.getMaxZoom()); } - _setMbCentroidProperties(mbMap: MbMap, mvtSourceLayer?: string) { + _setMbCentroidProperties( + mbMap: MbMap, + mvtSourceLayer?: string, + timesliceMaskConfig?: TimesliceMaskConfig + ) { const centroidLayerId = this._getMbCentroidLayerId(); const centroidLayer = mbMap.getLayer(centroidLayerId); if (!centroidLayer) { @@ -971,7 +1005,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.addLayer(mbLayer); } - const filterExpr = getCentroidFilterExpression(this.hasJoins()); + const filterExpr = getCentroidFilterExpression(this.hasJoins(), timesliceMaskConfig); if (!_.isEqual(filterExpr, mbMap.getFilter(centroidLayerId))) { mbMap.setFilter(centroidLayerId, filterExpr); } @@ -986,17 +1020,32 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.setLayerZoomRange(centroidLayerId, this.getMinZoom(), this.getMaxZoom()); } - _syncStylePropertiesWithMb(mbMap: MbMap) { - this._setMbPointsProperties(mbMap); - this._setMbLinePolygonProperties(mbMap); + _syncStylePropertiesWithMb(mbMap: MbMap, timeslice?: Timeslice) { + const timesliceMaskConfig = this._getTimesliceMaskConfig(timeslice); + this._setMbPointsProperties(mbMap, undefined, timesliceMaskConfig); + this._setMbLinePolygonProperties(mbMap, undefined, timesliceMaskConfig); // centroid layers added after polygon layers to ensure they are on top of polygon layers - this._setMbCentroidProperties(mbMap); + this._setMbCentroidProperties(mbMap, undefined, timesliceMaskConfig); } - syncLayerWithMB(mbMap: MbMap) { + _getTimesliceMaskConfig(timeslice?: Timeslice): TimesliceMaskConfig | undefined { + if (!timeslice || this.hasJoins()) { + return; + } + + const prevMeta = this.getSourceDataRequest()?.getMeta(); + return prevMeta !== undefined && prevMeta.timesiceMaskField !== undefined + ? { + timesiceMaskField: prevMeta.timesiceMaskField, + timeslice, + } + : undefined; + } + + syncLayerWithMB(mbMap: MbMap, timeslice?: Timeslice) { addGeoJsonMbSource(this._getMbSourceId(), this.getMbLayerIds(), mbMap); this._syncFeatureCollectionWithMb(mbMap); - this._syncStylePropertiesWithMb(mbMap); + this._syncStylePropertiesWithMb(mbMap, timeslice); } _getMbPointLayerId() { @@ -1094,6 +1143,15 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { return await this._source.getLicensedFeatures(); } + _getUpdateDueToTimesliceFromSourceRequestMeta(source: ISource, timeslice?: Timeslice) { + const prevDataRequest = this.getSourceDataRequest(); + const prevMeta = prevDataRequest?.getMeta(); + if (!prevMeta) { + return true; + } + return source.getUpdateDueToTimeslice(prevMeta, timeslice); + } + async addFeature(geometry: Geometry | Position[]) { const layerSource = this.getSource(); await layerSource.addFeature(geometry); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index a51e291574b70..9f7bd1260ca22 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -12,13 +12,19 @@ import { i18n } from '@kbn/i18n'; import { IFieldType, IndexPattern } from 'src/plugins/data/public'; import { GeoJsonProperties, Geometry, Position } from 'geojson'; import { AbstractESSource } from '../es_source'; -import { getHttp, getMapAppConfig, getSearchService } from '../../../kibana_services'; +import { + getHttp, + getMapAppConfig, + getSearchService, + getTimeFilter, +} from '../../../kibana_services'; import { addFieldToDSL, getField, hitsToGeoJson, isTotalHitsGreaterThan, PreIndexedShape, + TotalHits, } from '../../../../common/elasticsearch_util'; // @ts-expect-error import { UpdateSourceEditor } from './update_source_editor'; @@ -41,11 +47,14 @@ import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants'; import { ESDocField } from '../../fields/es_doc_field'; import { registerSource } from '../source_registry'; import { + DataMeta, ESSearchSourceDescriptor, + Timeslice, VectorSourceRequestMeta, VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters'; +import { TimeRange } from '../../../../../../../src/plugins/data/common'; import { ImmutableSourceProperty, SourceEditorArgs } from '../source'; import { IField } from '../../fields/field'; import { GeoJsonWithMeta, SourceTooltipConfig } from '../vector_source'; @@ -59,6 +68,16 @@ import { getDocValueAndSourceFields, ScriptField } from './util/get_docvalue_sou import { ITiledSingleLayerMvtParams } from '../tiled_single_layer_vector_source/tiled_single_layer_vector_source'; import { addFeatureToIndex, getMatchingIndexes } from './util/feature_edit'; +export function timerangeToTimeextent(timerange: TimeRange): Timeslice | undefined { + const timeRangeBounds = getTimeFilter().calculateBounds(timerange); + return timeRangeBounds.min !== undefined && timeRangeBounds.max !== undefined + ? { + from: timeRangeBounds.min.valueOf(), + to: timeRangeBounds.max.valueOf(), + } + : undefined; +} + export const sourceTitle = i18n.translate('xpack.maps.source.esSearchTitle', { defaultMessage: 'Documents', }); @@ -338,7 +357,6 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye async _getSearchHits( layerName: string, searchFilters: VectorSourceRequestMeta, - maxResultWindow: number, registerCancelCallback: (callback: () => void) => void ) { const indexPattern = await this.getIndexPattern(); @@ -350,8 +368,18 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye ); const initialSearchContext = { docvalue_fields: docValueFields }; // Request fields in docvalue_fields insted of _source + + // Use Kibana global time extent instead of timeslice extent when all documents for global time extent can be loaded + // to allow for client-side masking of timeslice + const searchFiltersWithoutTimeslice = { ...searchFilters }; + delete searchFiltersWithoutTimeslice.timeslice; + const useSearchFiltersWithoutTimeslice = + searchFilters.timeslice !== undefined && + (await this.canLoadAllDocuments(searchFiltersWithoutTimeslice, registerCancelCallback)); + + const maxResultWindow = await this.getMaxResultWindow(); const searchSource = await this.makeSearchSource( - searchFilters, + useSearchFiltersWithoutTimeslice ? searchFiltersWithoutTimeslice : searchFilters, maxResultWindow, initialSearchContext ); @@ -375,11 +403,17 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye searchSessionId: searchFilters.searchSessionId, }); + const isTimeExtentForTimeslice = + searchFilters.timeslice !== undefined && !useSearchFiltersWithoutTimeslice; return { hits: resp.hits.hits.reverse(), // Reverse hits so top documents by sort are drawn on top meta: { resultsCount: resp.hits.hits.length, areResultsTrimmed: isTotalHitsGreaterThan(resp.hits.total, resp.hits.hits.length), + timeExtent: isTimeExtentForTimeslice + ? searchFilters.timeslice + : timerangeToTimeextent(searchFilters.timeFilters), + isTimeExtentForTimeslice, }, }; } @@ -424,16 +458,9 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye ): Promise { const indexPattern = await this.getIndexPattern(); - const indexSettings = await loadIndexSettings(indexPattern.title); - const { hits, meta } = this._isTopHits() ? await this._getTopHits(layerName, searchFilters, registerCancelCallback) - : await this._getSearchHits( - layerName, - searchFilters, - indexSettings.maxResultWindow, - registerCancelCallback - ); + : await this._getSearchHits(layerName, searchFilters, registerCancelCallback); const unusedMetaFields = indexPattern.metaFields.filter((metaField) => { return !['_id', '_index'].includes(metaField); @@ -743,6 +770,62 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye : urlTemplate, }; } + + async getTimesliceMaskFieldName(): Promise { + if (this._isTopHits() || this._descriptor.scalingType === SCALING_TYPES.MVT) { + return null; + } + + const indexPattern = await this.getIndexPattern(); + return indexPattern.timeFieldName ? indexPattern.timeFieldName : null; + } + + getUpdateDueToTimeslice(prevMeta: DataMeta, timeslice?: Timeslice): boolean { + if (this._isTopHits() || this._descriptor.scalingType === SCALING_TYPES.MVT) { + return true; + } + + if ( + prevMeta.timeExtent === undefined || + prevMeta.areResultsTrimmed === undefined || + prevMeta.areResultsTrimmed + ) { + return true; + } + + const isTimeExtentForTimeslice = + prevMeta.isTimeExtentForTimeslice !== undefined ? prevMeta.isTimeExtentForTimeslice : false; + if (!timeslice) { + return isTimeExtentForTimeslice + ? // Previous request only covers timeslice extent. Will need to re-fetch data to cover global time extent + true + : // Previous request covers global time extent. + // No need to re-fetch data since previous request already has data for the entire global time extent. + false; + } + + const isWithin = isTimeExtentForTimeslice + ? timeslice.from >= prevMeta.timeExtent.from && timeslice.to <= prevMeta.timeExtent.to + : true; + return !isWithin; + } + + async canLoadAllDocuments( + searchFilters: VectorSourceRequestMeta, + registerCancelCallback: (callback: () => void) => void + ) { + const abortController = new AbortController(); + registerCancelCallback(() => abortController.abort()); + const maxResultWindow = await this.getMaxResultWindow(); + const searchSource = await this.makeSearchSource(searchFilters, 0); + searchSource.setField('trackTotalHits', maxResultWindow + 1); + const resp = await searchSource.fetch({ + abortSignal: abortController.signal, + sessionId: searchFilters.searchSessionId, + legacyHitsTotal: false, + }); + return !isTotalHitsGreaterThan((resp.hits.total as unknown) as TotalHits, maxResultWindow); + } } registerSource({ diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx index d58e71db2a9ab..5bf7a2e47cc66 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx @@ -228,6 +228,10 @@ export class MVTSingleLayerVectorSource return tooltips; } + async getTimesliceMaskFieldName() { + return null; + } + async supportsFeatureEditing(): Promise { return false; } diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts index 7a8fca337fd2e..0ecbde06cf3e2 100644 --- a/x-pack/plugins/maps/public/classes/sources/source.ts +++ b/x-pack/plugins/maps/public/classes/sources/source.ts @@ -13,7 +13,12 @@ import { GeoJsonProperties } from 'geojson'; import { copyPersistentState } from '../../reducers/copy_persistent_state'; import { IField } from '../fields/field'; import { FieldFormatter, LAYER_TYPE, MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; -import { AbstractSourceDescriptor, Attribution } from '../../../common/descriptor_types'; +import { + AbstractSourceDescriptor, + Attribution, + DataMeta, + Timeslice, +} from '../../../common/descriptor_types'; import { LICENSED_FEATURES } from '../../licensed_features'; import { PreIndexedShape } from '../../../common/elasticsearch_util'; @@ -64,6 +69,7 @@ export interface ISource { getMinZoom(): number; getMaxZoom(): number; getLicensedFeatures(): Promise; + getUpdateDueToTimeslice(prevMeta: DataMeta, timeslice?: Timeslice): boolean; } export class AbstractSource implements ISource { @@ -194,4 +200,8 @@ export class AbstractSource implements ISource { async getLicensedFeatures(): Promise { return []; } + + getUpdateDueToTimeslice(prevMeta: DataMeta, timeslice?: Timeslice): boolean { + return true; + } } diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx index 1194d571e344b..8f93de705e365 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx @@ -66,6 +66,7 @@ export interface IVectorSource extends ISource { getSupportedShapeTypes(): Promise; isBoundsAware(): boolean; getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig; + getTimesliceMaskFieldName(): Promise; supportsFeatureEditing(): Promise; addFeature(geometry: Geometry | Position[]): Promise; } @@ -156,6 +157,10 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc return null; } + async getTimesliceMaskFieldName(): Promise { + return null; + } + async addFeature(geometry: Geometry | Position[]) { throw new Error('Should implement VectorSource#addFeature'); } diff --git a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js index c13b2fd441cad..da3cbb9055d43 100644 --- a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js +++ b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js @@ -82,6 +82,9 @@ describe('updateDueToExtent', () => { describe('canSkipSourceUpdate', () => { const SOURCE_DATA_REQUEST_ID = 'foo'; + const getUpdateDueToTimeslice = () => { + return true; + }; describe('isQueryAware', () => { const queryAwareSourceMock = { @@ -136,6 +139,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); @@ -156,6 +160,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); @@ -176,6 +181,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -193,6 +199,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -224,6 +231,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -244,6 +252,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -264,6 +273,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -281,6 +291,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -327,6 +338,7 @@ describe('canSkipSourceUpdate', () => { applyGlobalTime: false, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -346,6 +358,7 @@ describe('canSkipSourceUpdate', () => { applyGlobalTime: true, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); @@ -375,6 +388,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -402,6 +416,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); @@ -429,6 +444,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); @@ -463,6 +479,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -498,6 +515,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -529,6 +547,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -564,6 +583,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); @@ -599,6 +619,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); diff --git a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts index 1f2678f40eecd..b6f03ef3d1c63 100644 --- a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts +++ b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts @@ -10,7 +10,7 @@ import turfBboxPolygon from '@turf/bbox-polygon'; import turfBooleanContains from '@turf/boolean-contains'; import { isRefreshOnlyQuery } from './is_refresh_only_query'; import { ISource } from '../sources/source'; -import { DataMeta } from '../../../common/descriptor_types'; +import { DataMeta, Timeslice } from '../../../common/descriptor_types'; import { DataRequest } from './data_request'; const SOURCE_UPDATE_REQUIRED = true; @@ -56,11 +56,13 @@ export async function canSkipSourceUpdate({ prevDataRequest, nextMeta, extentAware, + getUpdateDueToTimeslice, }: { source: ISource; prevDataRequest: DataRequest | undefined; nextMeta: DataMeta; extentAware: boolean; + getUpdateDueToTimeslice: (timeslice?: Timeslice) => boolean; }): Promise { const timeAware = await source.isTimeAware(); const refreshTimerAware = await source.isRefreshTimerAware(); @@ -94,7 +96,9 @@ export async function canSkipSourceUpdate({ updateDueToApplyGlobalTime = prevMeta.applyGlobalTime !== nextMeta.applyGlobalTime; if (nextMeta.applyGlobalTime) { updateDueToTime = !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters); - updateDueToTimeslice = !_.isEqual(prevMeta.timeslice, nextMeta.timeslice); + if (!_.isEqual(prevMeta.timeslice, nextMeta.timeslice)) { + updateDueToTimeslice = getUpdateDueToTimeslice(nextMeta.timeslice); + } } } diff --git a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts index f5df741759cb3..6a193216c7c1e 100644 --- a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts +++ b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts @@ -12,67 +12,110 @@ import { KBN_TOO_MANY_FEATURES_PROPERTY, } from '../../../common/constants'; +import { Timeslice } from '../../../common/descriptor_types'; + +export interface TimesliceMaskConfig { + timesiceMaskField: string; + timeslice: Timeslice; +} + export const EXCLUDE_TOO_MANY_FEATURES_BOX = ['!=', ['get', KBN_TOO_MANY_FEATURES_PROPERTY], true]; const EXCLUDE_CENTROID_FEATURES = ['!=', ['get', KBN_IS_CENTROID_FEATURE], true]; -function getFilterExpression(geometryFilter: unknown[], hasJoins: boolean) { - const filters: unknown[] = [ - EXCLUDE_TOO_MANY_FEATURES_BOX, - EXCLUDE_CENTROID_FEATURES, - geometryFilter, - ]; +function getFilterExpression( + filters: unknown[], + hasJoins: boolean, + timesliceMaskConfig?: TimesliceMaskConfig +) { + const allFilters: unknown[] = [...filters]; if (hasJoins) { - filters.push(['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]); + allFilters.push(['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]); } - return ['all', ...filters]; + if (timesliceMaskConfig) { + allFilters.push(['has', timesliceMaskConfig.timesiceMaskField]); + allFilters.push([ + '>=', + ['get', timesliceMaskConfig.timesiceMaskField], + timesliceMaskConfig.timeslice.from, + ]); + allFilters.push([ + '<', + ['get', timesliceMaskConfig.timesiceMaskField], + timesliceMaskConfig.timeslice.to, + ]); + } + + return ['all', ...allFilters]; } -export function getFillFilterExpression(hasJoins: boolean): unknown[] { +export function getFillFilterExpression( + hasJoins: boolean, + timesliceMaskConfig?: TimesliceMaskConfig +): unknown[] { return getFilterExpression( [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], + EXCLUDE_TOO_MANY_FEATURES_BOX, + EXCLUDE_CENTROID_FEATURES, + [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], + ], ], - hasJoins + hasJoins, + timesliceMaskConfig ); } -export function getLineFilterExpression(hasJoins: boolean): unknown[] { +export function getLineFilterExpression( + hasJoins: boolean, + timesliceMaskConfig?: TimesliceMaskConfig +): unknown[] { return getFilterExpression( [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING], + EXCLUDE_TOO_MANY_FEATURES_BOX, + EXCLUDE_CENTROID_FEATURES, + [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING], + ], ], - hasJoins + hasJoins, + timesliceMaskConfig ); } -export function getPointFilterExpression(hasJoins: boolean): unknown[] { +export function getPointFilterExpression( + hasJoins: boolean, + timesliceMaskConfig?: TimesliceMaskConfig +): unknown[] { return getFilterExpression( [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POINT], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT], + EXCLUDE_TOO_MANY_FEATURES_BOX, + EXCLUDE_CENTROID_FEATURES, + [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POINT], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT], + ], ], - hasJoins + hasJoins, + timesliceMaskConfig ); } -export function getCentroidFilterExpression(hasJoins: boolean): unknown[] { - const filters: unknown[] = [ - EXCLUDE_TOO_MANY_FEATURES_BOX, - ['==', ['get', KBN_IS_CENTROID_FEATURE], true], - ]; - - if (hasJoins) { - filters.push(['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]); - } - - return ['all', ...filters]; +export function getCentroidFilterExpression( + hasJoins: boolean, + timesliceMaskConfig?: TimesliceMaskConfig +): unknown[] { + return getFilterExpression( + [EXCLUDE_TOO_MANY_FEATURES_BOX, ['==', ['get', KBN_IS_CENTROID_FEATURE], true]], + hasJoins, + timesliceMaskConfig + ); } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/index.ts index 4f94cbc7b7458..b9b4b184318f5 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/index.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/index.ts @@ -27,6 +27,7 @@ import { getMapSettings, getScrollZoom, getSpatialFiltersLayer, + getTimeslice, } from '../../selectors/map_selectors'; import { getDrawMode, getIsFullScreen } from '../../selectors/ui_selectors'; import { getInspectorAdapters } from '../../reducers/non_serializable_instances'; @@ -43,6 +44,7 @@ function mapStateToProps(state: MapStoreState) { inspectorAdapters: getInspectorAdapters(state), scrollZoom: getScrollZoom(state), isFullScreen: getIsFullScreen(state), + timeslice: getTimeslice(state), featureModeActive: getDrawMode(state) === DRAW_MODE.DRAW_SHAPES || getDrawMode(state) === DRAW_MODE.DRAW_POINTS, filterModeActive: getDrawMode(state) === DRAW_MODE.DRAW_FILTERS, diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index 96ff7b7dcf882..2ce4e2d98ce5f 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -25,7 +25,7 @@ import { getInitialView } from './get_initial_view'; import { getPreserveDrawingBuffer } from '../../kibana_services'; import { ILayer } from '../../classes/layers/layer'; import { MapSettings } from '../../reducers/map'; -import { Goto, MapCenterAndZoom } from '../../../common/descriptor_types'; +import { Goto, MapCenterAndZoom, Timeslice } from '../../../common/descriptor_types'; import { DECIMAL_DEGREES_PRECISION, KBN_TOO_MANY_FEATURES_IMAGE_ID, @@ -68,13 +68,12 @@ export interface Props { onSingleValueTrigger?: (actionId: string, key: string, value: RawValue) => void; renderTooltipContent?: RenderToolTipContent; setAreTilesLoaded: (layerId: string, areTilesLoaded: boolean) => void; + timeslice?: Timeslice; featureModeActive: boolean; filterModeActive: boolean; } interface State { - prevLayerList: ILayer[] | undefined; - hasSyncedLayerList: boolean; mbMap: MapboxMap | undefined; } @@ -83,38 +82,23 @@ export class MBMap extends Component { private _isMounted: boolean = false; private _containerRef: HTMLDivElement | null = null; private _prevDisableInteractive?: boolean; + private _prevLayerList?: ILayer[]; + private _prevTimeslice?: Timeslice; private _navigationControl = new mapboxgl.NavigationControl({ showCompass: false }); private _tileStatusTracker?: TileStatusTracker; state: State = { - prevLayerList: undefined, - hasSyncedLayerList: false, mbMap: undefined, }; - static getDerivedStateFromProps(nextProps: Props, prevState: State) { - const nextLayerList = nextProps.layerList; - if (nextLayerList !== prevState.prevLayerList) { - return { - prevLayerList: nextLayerList, - hasSyncedLayerList: false, - }; - } - - return null; - } - componentDidMount() { this._initializeMap(); this._isMounted = true; } componentDidUpdate() { - if (this.state.mbMap) { - // do not debounce syncing of map-state - this._syncMbMapWithMapState(); - this._debouncedSync(); - } + this._syncMbMapWithMapState(); // do not debounce syncing of map-state + this._debouncedSync(); } componentWillUnmount() { @@ -134,16 +118,13 @@ export class MBMap extends Component { _debouncedSync = _.debounce(() => { if (this._isMounted && this.props.isMapReady && this.state.mbMap) { - if (!this.state.hasSyncedLayerList) { - this.setState( - { - hasSyncedLayerList: true, - }, - () => { - this._syncMbMapWithLayerList(); - this._syncMbMapWithInspector(); - } - ); + const hasLayerListChanged = this._prevLayerList !== this.props.layerList; // Comparing re-select memoized instance so no deep equals needed + const hasTimesliceChanged = !_.isEqual(this._prevTimeslice, this.props.timeslice); + if (hasLayerListChanged || hasTimesliceChanged) { + this._prevLayerList = this.props.layerList; + this._prevTimeslice = this.props.timeslice; + this._syncMbMapWithLayerList(); + this._syncMbMapWithInspector(); } this.props.spatialFiltersLayer.syncLayerWithMB(this.state.mbMap); this._syncSettings(); @@ -346,7 +327,9 @@ export class MBMap extends Component { this.props.layerList, this.props.spatialFiltersLayer ); - this.props.layerList.forEach((layer) => layer.syncLayerWithMB(this.state.mbMap!)); + this.props.layerList.forEach((layer) => + layer.syncLayerWithMB(this.state.mbMap!, this.props.timeslice) + ); syncLayerOrder(this.state.mbMap, this.props.spatialFiltersLayer, this.props.layerList); }; diff --git a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js index bf1b546baf131..70b2b3c383cf9 100644 --- a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js +++ b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js @@ -21,12 +21,12 @@ export default function ({ getPageObjects, getService }) { await security.testUser.restoreDefaults(); }); - it('should only fetch geo_point field and nothing else when source does not have data driven styling', async () => { + it('should only fetch geo_point field and time field and nothing else when source does not have data driven styling', async () => { await PageObjects.maps.loadSavedMap('document example'); const { rawResponse: response } = await PageObjects.maps.getResponse(); const firstHit = response.hits.hits[0]; expect(firstHit).to.only.have.keys(['_id', '_index', '_type', '_score', 'fields']); - expect(firstHit.fields).to.only.have.keys(['geo.coordinates']); + expect(firstHit.fields).to.only.have.keys(['@timestamp', 'geo.coordinates']); }); it('should only fetch geo_point field and data driven styling fields', async () => { @@ -34,7 +34,12 @@ export default function ({ getPageObjects, getService }) { const { rawResponse: response } = await PageObjects.maps.getResponse(); const firstHit = response.hits.hits[0]; expect(firstHit).to.only.have.keys(['_id', '_index', '_type', '_score', 'fields']); - expect(firstHit.fields).to.only.have.keys(['bytes', 'geo.coordinates', 'hour_of_day']); + expect(firstHit.fields).to.only.have.keys([ + '@timestamp', + 'bytes', + 'geo.coordinates', + 'hour_of_day', + ]); }); it('should format date fields as epoch_millis when data driven styling is applied to a date field', async () => { From 4c995eaa70eab7aecd22f0aa8520557cc032cb2c Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 15:01:09 -0400 Subject: [PATCH 29/43] [docker] Removes setting with hyphen (#103085) (#103136) This setting is causing an error to be throw as it's being used in an environment variable. Created https://github.com/elastic/kibana/issues/103084 Signed-off-by: Tyler Smalley Co-authored-by: Tyler Smalley --- .../docker_generator/resources/base/bin/kibana-docker | 1 - 1 file changed, 1 deletion(-) diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 8e9a6003441c8..c5a2ce1378362 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -69,7 +69,6 @@ kibana_vars=( logging.appenders logging.appenders.console logging.appenders.file - logging.appenders.rolling-file logging.dest logging.json logging.loggers From 89c4acf5f8bcdcf798ec7afd302ac85b2a48ad88 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 15:01:52 -0400 Subject: [PATCH 30/43] [Maps] Use id-values from client-manifest to suggest layers (#102788) (#103135) Co-authored-by: Thomas Neirynck --- .../keyword_content.tsx | 8 - .../ems_autosuggest/ems_autosuggest.test.ts | 119 ++++------- .../public/ems_autosuggest/ems_autosuggest.ts | 187 +++++++----------- .../ml/common/constants/embeddable_map.ts | 13 -- .../application/explorer/anomalies_map.tsx | 2 - 5 files changed, 113 insertions(+), 216 deletions(-) delete mode 100644 x-pack/plugins/ml/common/constants/embeddable_map.ts diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/keyword_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/keyword_content.tsx index 22fe8244ef760..1baea4b3f2f7c 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/keyword_content.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/keyword_content.tsx @@ -14,13 +14,6 @@ import { DocumentStatsTable } from './document_stats'; import { ExpandedRowContent } from './expanded_row_content'; import { ChoroplethMap } from './choropleth_map'; -const COMMON_EMS_LAYER_IDS = [ - 'world_countries', - 'administrative_regions_lvl2', - 'usa_zip_codes', - 'usa_states', -]; - export const KeywordContent: FC = ({ config }) => { const [EMSSuggestion, setEMSSuggestion] = useState(); const { stats, fieldName } = config; @@ -32,7 +25,6 @@ export const KeywordContent: FC = ({ config }) => { const loadEMSTermSuggestions = useCallback(async () => { if (!mapsPlugin) return; const suggestion: EMSTermJoinConfig | null = await mapsPlugin.suggestEMSTermJoinConfig({ - emsLayerIds: COMMON_EMS_LAYER_IDS, sampleValues: Array.isArray(stats?.topValues) ? stats?.topValues.map((value) => value.key) : [], diff --git a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts index eff49c1b1242e..cc0ed19db0b40 100644 --- a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts +++ b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts @@ -6,40 +6,22 @@ */ import { suggestEMSTermJoinConfig } from './ems_autosuggest'; -import { FeatureCollection } from 'geojson'; class MockFileLayer { - private readonly _url: string; private readonly _id: string; private readonly _fields: Array<{ id: string }>; - constructor(url: string, fields: Array<{ id: string }>) { - this._url = url; - this._id = url; + constructor(id: string, fields: Array<{ id: string; alias?: string[]; values?: string[] }>) { + this._id = id; this._fields = fields; } - getFields() { - return this._fields; + getId() { + return this._id; } - getGeoJson() { - if (this._url === 'world_countries') { - return ({ - type: 'FeatureCollection', - features: [ - { properties: { iso2: 'CA', iso3: 'CAN' } }, - { properties: { iso2: 'US', iso3: 'USA' } }, - ], - } as unknown) as FeatureCollection; - } else if (this._url === 'zips') { - return ({ - type: 'FeatureCollection', - features: [{ properties: { zip: '40204' } }, { properties: { zip: '40205' } }], - } as unknown) as FeatureCollection; - } else { - throw new Error(`unrecognized mock url ${this._url}`); - } + getFields() { + return this._fields; } hasId(id: string) { @@ -51,31 +33,31 @@ jest.mock('../util', () => { return { async getEmsFileLayers() { return [ - new MockFileLayer('world_countries', [{ id: 'iso2' }, { id: 'iso3' }]), - new MockFileLayer('zips', [{ id: 'zip' }]), + new MockFileLayer('world_countries', [ + { + id: 'iso2', + alias: ['(geo\\.){0,}country_iso_code$', '(country|countries)'], + values: ['CA', 'US'], + }, + { id: 'iso3', values: ['CAN', 'USA'] }, + { id: 'name', alias: ['(country|countries)'] }, + ]), + new MockFileLayer('usa_zip_codes', [ + { id: 'zip', alias: ['zip'], values: ['40204', '40205'] }, + ]), ]; }, }; }); describe('suggestEMSTermJoinConfig', () => { - test('no info provided', async () => { + test('Should not validate when no info provided', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({}); expect(termJoinConfig).toBe(null); }); - describe('validate common column names', () => { - test('ecs region', async () => { - const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValuesColumnName: 'destination.geo.region_iso_code', - }); - expect(termJoinConfig).toEqual({ - layerId: 'administrative_regions_lvl2', - field: 'region_iso_code', - }); - }); - - test('ecs country', async () => { + describe('With common column names', () => { + test('should match first match', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ sampleValuesColumnName: 'country_iso_code', }); @@ -85,78 +67,61 @@ describe('suggestEMSTermJoinConfig', () => { }); }); - test('country', async () => { + test('When sampleValues are provided, should reject match if no sampleValues for a layer, even though the name matches', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValuesColumnName: 'Country_name', - }); - expect(termJoinConfig).toEqual({ - layerId: 'world_countries', - field: 'name', + sampleValuesColumnName: 'country_iso_code', + sampleValues: ['FO', 'US', 'CA'], }); + expect(termJoinConfig).toEqual(null); }); - test('unknown name', async () => { + test('should reject match if sampleValues not in id-list', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValuesColumnName: 'cntry', + sampleValuesColumnName: 'zip', + sampleValues: ['90201', '40205'], }); expect(termJoinConfig).toEqual(null); }); - }); - describe('validate well known formats', () => { - test('5-digit zip code', async () => { + test('should return first match (regex matches both iso2 and name)', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValues: ['90201', 40204], + sampleValuesColumnName: 'Country_name', }); expect(termJoinConfig).toEqual({ - layerId: 'usa_zip_codes', - field: 'zip', + layerId: 'world_countries', + field: 'iso2', }); }); - test('mismatch', async () => { + test('unknown name', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValues: ['90201', 'foobar'], + sampleValuesColumnName: 'cntry', }); expect(termJoinConfig).toEqual(null); }); }); - describe('validate based on EMS data', () => { - test('Should validate with zip codes layer', async () => { + describe('validate well known formats (using id-values in manifest)', () => { + test('Should validate known zipcodes', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValues: ['40204', 40205], - emsLayerIds: ['world_countries', 'zips'], + sampleValues: ['40205', 40204], }); expect(termJoinConfig).toEqual({ - layerId: 'zips', + layerId: 'usa_zip_codes', field: 'zip', }); }); - test('Should not validate with faulty zip codes', async () => { + test('Should not validate unknown zipcode (in this case, 90201)', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValues: ['40204', '00000'], - emsLayerIds: ['world_countries', 'zips'], + sampleValues: ['90201', 40204], }); expect(termJoinConfig).toEqual(null); }); - test('Should validate against countries', async () => { + test('Should not validate mismatches', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValues: ['USA', 'USA', 'CAN'], - emsLayerIds: ['world_countries', 'zips'], - }); - expect(termJoinConfig).toEqual({ - layerId: 'world_countries', - field: 'iso3', - }); - }); - - test('Should not validate against missing countries', async () => { - const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValues: ['USA', 'BEL', 'CAN'], - emsLayerIds: ['world_countries', 'zips'], + sampleValues: ['90201', 'foobar'], }); expect(termJoinConfig).toEqual(null); }); diff --git a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts index 952e48a71a9dc..66fcbd805f53e 100644 --- a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts +++ b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts @@ -7,10 +7,8 @@ import type { FileLayer } from '@elastic/ems-client'; import { getEmsFileLayers } from '../util'; -import { emsWorldLayerId, emsRegionLayerId, emsUsaZipLayerId } from '../../common'; export interface SampleValuesConfig { - emsLayerIds?: string[]; sampleValues?: Array; sampleValuesColumnName?: string; } @@ -20,44 +18,16 @@ export interface EMSTermJoinConfig { field: string; } -const wellKnownColumnNames = [ - { - regex: /(geo\.){0,}country_iso_code$/i, // ECS postfix for country - emsConfig: { - layerId: emsWorldLayerId, - field: 'iso2', - }, - }, - { - regex: /(geo\.){0,}region_iso_code$/i, // ECS postfixn for region - emsConfig: { - layerId: emsRegionLayerId, - field: 'region_iso_code', - }, - }, - { - regex: /^country/i, // anything starting with country - emsConfig: { - layerId: emsWorldLayerId, - field: 'name', - }, - }, -]; - -const wellKnownColumnFormats = [ - { - regex: /(^\d{5}$)/i, // 5-digit zipcode - emsConfig: { - layerId: emsUsaZipLayerId, - field: 'zip', - }, - }, -]; - interface UniqueMatch { - config: { layerId: string; field: string }; + config: EMSTermJoinConfig; count: number; } +interface FileLayerFieldShim { + id: string; + values?: string[]; + regex?: string; + alias?: string[]; +} export async function suggestEMSTermJoinConfig( sampleValuesConfig: SampleValuesConfig @@ -65,20 +35,17 @@ export async function suggestEMSTermJoinConfig( const matches: EMSTermJoinConfig[] = []; if (sampleValuesConfig.sampleValuesColumnName) { - matches.push(...suggestByName(sampleValuesConfig.sampleValuesColumnName)); + const matchesBasedOnColumnName = await suggestByName( + sampleValuesConfig.sampleValuesColumnName, + sampleValuesConfig.sampleValues + ); + matches.push(...matchesBasedOnColumnName); } if (sampleValuesConfig.sampleValues && sampleValuesConfig.sampleValues.length) { - if (sampleValuesConfig.emsLayerIds && sampleValuesConfig.emsLayerIds.length) { - matches.push( - ...(await suggestByEMSLayerIds( - sampleValuesConfig.emsLayerIds, - sampleValuesConfig.sampleValues - )) - ); - } else { - matches.push(...suggestByValues(sampleValuesConfig.sampleValues)); - } + // Only looks at id-values in main manifest + const matchesBasedOnIds = await suggestByIdValues(sampleValuesConfig.sampleValues); + matches.push(...matchesBasedOnIds); } const uniqMatches: UniqueMatch[] = matches.reduce((accum: UniqueMatch[], match) => { @@ -105,92 +72,80 @@ export async function suggestEMSTermJoinConfig( return uniqMatches.length ? uniqMatches[0].config : null; } -function suggestByName(columnName: string): EMSTermJoinConfig[] { - const matches = wellKnownColumnNames.filter((wellknown) => { - return columnName.match(wellknown.regex); - }); - - return matches.map((m) => { - return m.emsConfig; - }); -} +async function suggestByName( + columnName: string, + sampleValues?: Array +): Promise { + const fileLayers = await getEmsFileLayers(); -function suggestByValues(values: Array): EMSTermJoinConfig[] { - const matches = wellKnownColumnFormats.filter((wellknown) => { - for (let i = 0; i < values.length; i++) { - const value = values[i].toString(); - if (!value.match(wellknown.regex)) { - return false; + const matches: EMSTermJoinConfig[] = []; + fileLayers.forEach((fileLayer) => { + const emsFields: FileLayerFieldShim[] = fileLayer.getFields(); + emsFields.forEach((emsField: FileLayerFieldShim) => { + if (!emsField.alias || !emsField.alias.length) { + return; } - } - return true; - }); - return matches.map((m) => { - return m.emsConfig; + const emsConfig = { + layerId: fileLayer.getId(), + field: emsField.id, + }; + emsField.alias.forEach((alias: string) => { + const regex = new RegExp(alias, 'i'); + const nameMatchesAlias = !!columnName.match(regex); + // Check if this violates any known id-values. + + let isMatch: boolean; + if (sampleValues) { + if (emsField.values && emsField.values.length) { + isMatch = nameMatchesAlias && allSamplesMatch(sampleValues, emsField.values); + } else { + // requires validation against sample-values but EMS provides no meta to do so. + isMatch = false; + } + } else { + isMatch = nameMatchesAlias; + } + + if (isMatch) { + matches.push(emsConfig); + } + }); + }); }); -} -function existsInEMS(emsJson: any, emsFieldId: string, sampleValue: string): boolean { - for (let i = 0; i < emsJson.features.length; i++) { - const emsFieldValue = emsJson.features[i].properties[emsFieldId].toString(); - if (emsFieldValue.toString() === sampleValue) { - return true; - } - } - return false; + return matches; } -function matchesEmsField(emsJson: any, emsFieldId: string, sampleValues: Array) { +function allSamplesMatch(sampleValues: Array, ids: string[]) { for (let j = 0; j < sampleValues.length; j++) { const sampleValue = sampleValues[j].toString(); - if (!existsInEMS(emsJson, emsFieldId, sampleValue)) { + if (!ids.includes(sampleValue)) { return false; } } return true; } -async function getMatchesForEMSLayer( - emsLayerId: string, +async function suggestByIdValues( sampleValues: Array ): Promise { + const matches: EMSTermJoinConfig[] = []; const fileLayers: FileLayer[] = await getEmsFileLayers(); - const emsFileLayer: FileLayer | undefined = fileLayers.find((fl: FileLayer) => - fl.hasId(emsLayerId) - ); - - if (!emsFileLayer) { - return []; - } - - const emsFields = emsFileLayer.getFields(); - - try { - const emsJson = await emsFileLayer.getGeoJson(); - const matches: EMSTermJoinConfig[] = []; - for (let f = 0; f < emsFields.length; f++) { - if (matchesEmsField(emsJson, emsFields[f].id, sampleValues)) { - matches.push({ - layerId: emsLayerId, - field: emsFields[f].id, - }); + fileLayers.forEach((fileLayer) => { + const emsFields: FileLayerFieldShim[] = fileLayer.getFields(); + emsFields.forEach((emsField: FileLayerFieldShim) => { + if (!emsField.values || !emsField.values.length) { + return; } - } - return matches; - } catch (e) { - return []; - } -} - -async function suggestByEMSLayerIds( - emsLayerIds: string[], - values: Array -): Promise { - const matches = []; - for (const emsLayerId of emsLayerIds) { - const layerIdMathes = await getMatchesForEMSLayer(emsLayerId, values); - matches.push(...layerIdMathes); - } + const emsConfig = { + layerId: fileLayer.getId(), + field: emsField.id, + }; + if (allSamplesMatch(sampleValues, emsField.values)) { + matches.push(emsConfig); + } + }); + }); return matches; } diff --git a/x-pack/plugins/ml/common/constants/embeddable_map.ts b/x-pack/plugins/ml/common/constants/embeddable_map.ts deleted file mode 100644 index 6cb345bae630e..0000000000000 --- a/x-pack/plugins/ml/common/constants/embeddable_map.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const COMMON_EMS_LAYER_IDS = [ - 'world_countries', - 'administrative_regions_lvl2', - 'usa_zip_codes', - 'usa_states', -]; diff --git a/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx b/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx index 73a6a9d64b60e..fe43bd659131f 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx @@ -28,7 +28,6 @@ import { isDefined } from '../../../common/types/guards'; import { MlEmbeddedMapComponent } from '../components/ml_embedded_map'; import { EMSTermJoinConfig } from '../../../../maps/public'; import { AnomaliesTableRecord } from '../../../common/types/anomalies'; -import { COMMON_EMS_LAYER_IDS } from '../../../common/constants/embeddable_map'; const MAX_ENTITY_VALUES = 3; @@ -177,7 +176,6 @@ export const AnomaliesMap: FC = ({ anomalies, jobIds }) => { } const suggestion: EMSTermJoinConfig | null = await mapsPlugin.suggestEMSTermJoinConfig({ - emsLayerIds: COMMON_EMS_LAYER_IDS, sampleValues: Array.from(entityValues), sampleValuesColumnName: entityName || '', }); From 2192b0875dc0f6c99a0c03002f919551a3e1ec9c Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 23 Jun 2021 15:14:56 -0400 Subject: [PATCH 31/43] [Exploratory view] Refactor code for multi series (#101157) (#103139) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Shahzad --- .../app/RumDashboard/ActionMenu/index.tsx | 5 +- .../PageLoadDistribution/index.tsx | 2 +- .../app/RumDashboard/PageViewsTrend/index.tsx | 2 +- .../components/empty_view.tsx | 8 +- .../components/filter_label.test.tsx | 4 + .../components/filter_label.tsx | 6 +- .../configurations/constants/constants.ts | 22 +- .../configurations/default_configs.ts | 14 +- .../configurations/lens_attributes.test.ts | 303 +++++----- .../configurations/lens_attributes.ts | 530 +++++++++++------- .../mobile/device_distribution_config.ts | 2 +- .../synthetics/data_distribution_config.ts | 4 +- .../test_data/sample_attribute.ts | 28 +- .../exploratory_view/configurations/utils.ts | 40 +- .../exploratory_view.test.tsx | 3 +- .../exploratory_view/exploratory_view.tsx | 105 +++- .../exploratory_view/header/header.test.tsx | 2 +- .../shared/exploratory_view/header/header.tsx | 7 +- .../hooks/use_app_index_pattern.tsx | 35 +- .../hooks/use_lens_attributes.ts | 79 ++- .../hooks/use_series_storage.tsx | 29 +- .../shared/exploratory_view/index.tsx | 4 +- .../shared/exploratory_view/rtl_helpers.tsx | 11 +- .../series_builder/columns/chart_types.tsx | 8 +- .../columns/data_types_col.test.tsx | 11 +- .../series_builder/columns/data_types_col.tsx | 6 +- .../columns/date_picker_col.tsx | 11 +- .../columns/operation_type_select.test.tsx | 8 +- .../columns/report_breakdowns.test.tsx | 6 +- .../columns/report_definition_col.test.tsx | 6 +- .../columns/report_definition_col.tsx | 32 +- .../columns/report_definition_field.tsx | 26 +- .../columns/report_filters.test.tsx | 2 +- .../columns/report_types_col.test.tsx | 11 +- .../columns/report_types_col.tsx | 28 +- .../series_builder/last_updated.tsx | 37 ++ .../series_builder/series_builder.tsx | 285 +++++++--- .../series_date_picker/date_range_picker.tsx | 113 ++++ .../series_date_picker/index.tsx | 2 +- .../series_date_picker.test.tsx | 10 +- .../series_editor/columns/breakdowns.test.tsx | 4 +- .../series_editor/columns/date_picker_col.tsx | 11 +- .../series_editor/columns/filter_expanded.tsx | 8 +- .../columns/filter_value_btn.test.tsx | 4 +- .../columns/filter_value_btn.tsx | 4 +- .../series_editor/columns/remove_series.tsx | 4 +- .../series_editor/columns/series_actions.tsx | 92 ++- .../series_editor/selected_filters.test.tsx | 2 +- .../series_editor/selected_filters.tsx | 5 +- .../series_editor/series_editor.tsx | 128 ++--- .../shared/exploratory_view/types.ts | 7 +- .../utils/stringify_kueries.test.ts | 148 +++++ .../utils/stringify_kueries.ts | 37 ++ .../observability/public/routes/index.tsx | 14 + .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../common/charts/ping_histogram.tsx | 2 +- .../common/header/action_menu_content.tsx | 5 +- .../monitor_duration_container.tsx | 2 +- 59 files changed, 1556 insertions(+), 770 deletions(-) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.test.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx index 20d930d28599f..63ba7047696ca 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx @@ -47,10 +47,11 @@ export function UXActionMenu({ const uxExploratoryViewLink = createExploratoryViewUrl( { - 'ux-series': { + 'ux-series': ({ dataType: 'ux', + isNew: true, time: { from: rangeFrom, to: rangeTo }, - } as SeriesUrl, + } as unknown) as SeriesUrl, }, http?.basePath.get() ); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index e0486af6cd6ef..5c63cc24b6fdf 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -89,7 +89,7 @@ export function PageLoadDistribution() { { [`${serviceName}-page-views`]: { dataType: 'ux', - reportType: 'dist', + reportType: 'data-distribution', time: { from: rangeFrom!, to: rangeTo! }, reportDefinitions: { 'service.name': serviceName as string[], diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx index c45637e5d3c82..667d0b5e4b4db 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx @@ -64,7 +64,7 @@ export function PageViewsTrend() { { [`${serviceName}-page-views`]: { dataType: 'ux', - reportType: 'kpi', + reportType: 'kpi-over-time', time: { from: rangeFrom!, to: rangeTo! }, reportDefinitions: { 'service.name': serviceName as string[], diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx index ea69a371cedae..3566835b1701c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx @@ -38,6 +38,12 @@ export function EmptyView({ emptyMessage = SELECTED_DATA_TYPE_FOR_REPORT; } + if (!series) { + emptyMessage = i18n.translate('xpack.observability.expView.seriesEditor.notFound', { + defaultMessage: 'No series found. Please add a series.', + }); + } + return ( {loading && ( @@ -77,7 +83,7 @@ export const EMPTY_LABEL = i18n.translate('xpack.observability.expView.seriesBui export const CHOOSE_REPORT_DEFINITION = i18n.translate( 'xpack.observability.expView.seriesBuilder.emptyReportDefinition', { - defaultMessage: 'Select a report type to create a visualization.', + defaultMessage: 'Select a report definition to create a visualization.', } ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx index af64e74bca89c..fe2953edd36d6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx @@ -29,6 +29,7 @@ describe('FilterLabel', function () { negate={false} seriesId={'kpi-over-time'} removeFilter={jest.fn()} + indexPattern={mockIndexPattern} /> ); @@ -52,6 +53,7 @@ describe('FilterLabel', function () { negate={false} seriesId={'kpi-over-time'} removeFilter={removeFilter} + indexPattern={mockIndexPattern} /> ); @@ -74,6 +76,7 @@ describe('FilterLabel', function () { negate={false} seriesId={'kpi-over-time'} removeFilter={removeFilter} + indexPattern={mockIndexPattern} /> ); @@ -99,6 +102,7 @@ describe('FilterLabel', function () { negate={true} seriesId={'kpi-over-time'} removeFilter={jest.fn()} + indexPattern={mockIndexPattern} /> ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx index 3d4ba6dc08c37..a08e777c5ea71 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { IndexPattern } from '../../../../../../../../src/plugins/data/public'; import { useSeriesFilters } from '../hooks/use_series_filters'; import { FilterValueLabel } from '../../filter_value_label/filter_value_label'; @@ -17,6 +17,7 @@ interface Props { seriesId: string; negate: boolean; definitionFilter?: boolean; + indexPattern: IndexPattern; removeFilter: (field: string, value: string, notVal: boolean) => void; } @@ -26,11 +27,10 @@ export function FilterLabel({ field, value, negate, + indexPattern, removeFilter, definitionFilter, }: Props) { - const { indexPattern } = useAppIndexPatternContext(); - const { invertFilter } = useSeriesFilters({ seriesId }); return indexPattern ? ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index e119507860c5c..01e8d023ae96b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -5,8 +5,15 @@ * 2.0. */ -import { ReportViewTypeId } from '../../types'; -import { CLS_FIELD, FCP_FIELD, FID_FIELD, LCP_FIELD, TBT_FIELD } from './elasticsearch_fieldnames'; +import { ReportViewType } from '../../types'; +import { + CLS_FIELD, + FCP_FIELD, + FID_FIELD, + LCP_FIELD, + TBT_FIELD, + TRANSACTION_TIME_TO_FIRST_BYTE, +} from './elasticsearch_fieldnames'; import { AGENT_HOST_LABEL, BROWSER_FAMILY_LABEL, @@ -58,6 +65,7 @@ export const FieldLabels: Record = { [TBT_FIELD]: TBT_LABEL, [FID_FIELD]: FID_LABEL, [CLS_FIELD]: CLS_LABEL, + [TRANSACTION_TIME_TO_FIRST_BYTE]: 'Page load time', 'monitor.id': MONITOR_ID_LABEL, 'monitor.status': MONITOR_STATUS_LABEL, @@ -77,11 +85,11 @@ export const FieldLabels: Record = { 'http.request.method': REQUEST_METHOD, }; -export const DataViewLabels: Record = { - dist: PERF_DIST_LABEL, - kpi: KPI_OVER_TIME_LABEL, - cwv: CORE_WEB_VITALS_LABEL, - mdd: DEVICE_DISTRIBUTION_LABEL, +export const DataViewLabels: Record = { + 'data-distribution': PERF_DIST_LABEL, + 'kpi-over-time': KPI_OVER_TIME_LABEL, + 'core-web-vitals': CORE_WEB_VITALS_LABEL, + 'device-data-distribution': DEVICE_DISTRIBUTION_LABEL, }; export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts index 07342d976cbea..574a9f6a2bc10 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AppDataType, ReportViewTypes } from '../types'; +import { AppDataType, ReportViewType } from '../types'; import { getRumDistributionConfig } from './rum/data_distribution_config'; import { getSyntheticsDistributionConfig } from './synthetics/data_distribution_config'; import { getSyntheticsKPIConfig } from './synthetics/kpi_over_time_config'; @@ -17,7 +17,7 @@ import { getMobileKPIDistributionConfig } from './mobile/distribution_config'; import { getMobileDeviceDistributionConfig } from './mobile/device_distribution_config'; interface Props { - reportType: keyof typeof ReportViewTypes; + reportType: ReportViewType; indexPattern: IndexPattern; dataType: AppDataType; } @@ -25,23 +25,23 @@ interface Props { export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props) => { switch (dataType) { case 'ux': - if (reportType === 'dist') { + if (reportType === 'data-distribution') { return getRumDistributionConfig({ indexPattern }); } - if (reportType === 'cwv') { + if (reportType === 'core-web-vitals') { return getCoreWebVitalsConfig({ indexPattern }); } return getKPITrendsLensConfig({ indexPattern }); case 'synthetics': - if (reportType === 'dist') { + if (reportType === 'data-distribution') { return getSyntheticsDistributionConfig({ indexPattern }); } return getSyntheticsKPIConfig({ indexPattern }); case 'mobile': - if (reportType === 'dist') { + if (reportType === 'data-distribution') { return getMobileKPIDistributionConfig({ indexPattern }); } - if (reportType === 'mdd') { + if (reportType === 'device-data-distribution') { return getMobileDeviceDistributionConfig({ indexPattern }); } return getMobileKPIConfig({ indexPattern }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index 8b21df64a3c91..5189a529bda8f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -5,25 +5,37 @@ * 2.0. */ -import { LensAttributes } from './lens_attributes'; +import { LayerConfig, LensAttributes } from './lens_attributes'; import { mockAppIndexPattern, mockIndexPattern } from '../rtl_helpers'; import { getDefaultConfigs } from './default_configs'; import { sampleAttribute } from './test_data/sample_attribute'; -import { LCP_FIELD, SERVICE_NAME, USER_AGENT_NAME } from './constants/elasticsearch_fieldnames'; +import { LCP_FIELD, USER_AGENT_NAME } from './constants/elasticsearch_fieldnames'; +import { buildExistsFilter, buildPhrasesFilter } from './utils'; describe('Lens Attribute', () => { mockAppIndexPattern(); const reportViewConfig = getDefaultConfigs({ - reportType: 'dist', + reportType: 'data-distribution', dataType: 'ux', indexPattern: mockIndexPattern, }); + reportViewConfig.filters?.push(...buildExistsFilter('transaction.type', mockIndexPattern)); + let lnsAttr: LensAttributes; + const layerConfig: LayerConfig = { + reportConfig: reportViewConfig, + seriesType: 'line', + operationType: 'count', + indexPattern: mockIndexPattern, + reportDefinitions: {}, + time: { from: 'now-15m', to: 'now' }, + }; + beforeEach(() => { - lnsAttr = new LensAttributes(mockIndexPattern, reportViewConfig, 'line', [], 'count', {}); + lnsAttr = new LensAttributes([layerConfig]); }); it('should return expected json', function () { @@ -31,7 +43,7 @@ describe('Lens Attribute', () => { }); it('should return main y axis', function () { - expect(lnsAttr.getMainYAxis()).toEqual({ + expect(lnsAttr.getMainYAxis(layerConfig)).toEqual({ dataType: 'number', isBucketed: false, label: 'Pages loaded', @@ -42,7 +54,7 @@ describe('Lens Attribute', () => { }); it('should return expected field type', function () { - expect(JSON.stringify(lnsAttr.getFieldMeta('transaction.type'))).toEqual( + expect(JSON.stringify(lnsAttr.getFieldMeta('transaction.type', layerConfig))).toEqual( JSON.stringify({ fieldMeta: { count: 0, @@ -60,7 +72,7 @@ describe('Lens Attribute', () => { }); it('should return expected field type for custom field with default value', function () { - expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual( + expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric', layerConfig))).toEqual( JSON.stringify({ fieldMeta: { count: 0, @@ -79,11 +91,18 @@ describe('Lens Attribute', () => { }); it('should return expected field type for custom field with passed value', function () { - lnsAttr = new LensAttributes(mockIndexPattern, reportViewConfig, 'line', [], 'count', { - 'performance.metric': [LCP_FIELD], - }); + const layerConfig1: LayerConfig = { + reportConfig: reportViewConfig, + seriesType: 'line', + operationType: 'count', + indexPattern: mockIndexPattern, + reportDefinitions: { 'performance.metric': [LCP_FIELD] }, + time: { from: 'now-15m', to: 'now' }, + }; - expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual( + lnsAttr = new LensAttributes([layerConfig1]); + + expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric', layerConfig1))).toEqual( JSON.stringify({ fieldMeta: { count: 0, @@ -102,7 +121,7 @@ describe('Lens Attribute', () => { }); it('should return expected number range column', function () { - expect(lnsAttr.getNumberRangeColumn('transaction.duration.us')).toEqual({ + expect(lnsAttr.getNumberRangeColumn('transaction.duration.us', reportViewConfig)).toEqual({ dataType: 'number', isBucketed: true, label: 'Page load time', @@ -124,7 +143,7 @@ describe('Lens Attribute', () => { }); it('should return expected number operation column', function () { - expect(lnsAttr.getNumberRangeColumn('transaction.duration.us')).toEqual({ + expect(lnsAttr.getNumberRangeColumn('transaction.duration.us', reportViewConfig)).toEqual({ dataType: 'number', isBucketed: true, label: 'Page load time', @@ -160,7 +179,7 @@ describe('Lens Attribute', () => { }); it('should return main x axis', function () { - expect(lnsAttr.getXAxis()).toEqual({ + expect(lnsAttr.getXAxis(layerConfig, 'layer0')).toEqual({ dataType: 'number', isBucketed: true, label: 'Page load time', @@ -182,38 +201,45 @@ describe('Lens Attribute', () => { }); it('should return first layer', function () { - expect(lnsAttr.getLayer()).toEqual({ - columnOrder: ['x-axis-column', 'y-axis-column'], - columns: { - 'x-axis-column': { - dataType: 'number', - isBucketed: true, - label: 'Page load time', - operationType: 'range', - params: { - maxBars: 'auto', - ranges: [ - { - from: 0, - label: '', - to: 1000, - }, - ], - type: 'histogram', + expect(lnsAttr.getLayers()).toEqual({ + layer0: { + columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'], + columns: { + 'x-axis-column-layer0': { + dataType: 'number', + isBucketed: true, + label: 'Page load time', + operationType: 'range', + params: { + maxBars: 'auto', + ranges: [ + { + from: 0, + label: '', + to: 1000, + }, + ], + type: 'histogram', + }, + scale: 'interval', + sourceField: 'transaction.duration.us', + }, + 'y-axis-column-layer0': { + dataType: 'number', + isBucketed: false, + label: 'Pages loaded', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + filter: { + language: 'kuery', + query: + 'transaction.type: page-load and processor.event: transaction and transaction.type : *', + }, }, - scale: 'interval', - sourceField: 'transaction.duration.us', - }, - 'y-axis-column': { - dataType: 'number', - isBucketed: false, - label: 'Pages loaded', - operationType: 'count', - scale: 'ratio', - sourceField: 'Records', }, + incompleteColumns: {}, }, - incompleteColumns: {}, }); }); @@ -225,12 +251,12 @@ describe('Lens Attribute', () => { gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, layers: [ { - accessors: ['y-axis-column'], - layerId: 'layer1', + accessors: ['y-axis-column-layer0'], + layerId: 'layer0', palette: undefined, seriesType: 'line', - xAccessor: 'x-axis-column', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }], + xAccessor: 'x-axis-column-layer0', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], }, ], legend: { isVisible: true, position: 'right' }, @@ -240,108 +266,52 @@ describe('Lens Attribute', () => { }); }); - describe('ParseFilters function', function () { - it('should parse default filters', function () { - expect(lnsAttr.parseFilters()).toEqual([ - { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } }, - { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } }, - ]); - }); - - it('should parse default and ui filters', function () { - lnsAttr = new LensAttributes( - mockIndexPattern, - reportViewConfig, - 'line', - [ - { field: SERVICE_NAME, values: ['elastic-co', 'kibana-front'] }, - { field: USER_AGENT_NAME, values: ['Firefox'], notValues: ['Chrome'] }, - ], - 'count', - {} - ); + describe('Layer breakdowns', function () { + it('should return breakdown column', function () { + const layerConfig1: LayerConfig = { + reportConfig: reportViewConfig, + seriesType: 'line', + operationType: 'count', + indexPattern: mockIndexPattern, + reportDefinitions: { 'performance.metric': [LCP_FIELD] }, + breakdown: USER_AGENT_NAME, + time: { from: 'now-15m', to: 'now' }, + }; - expect(lnsAttr.parseFilters()).toEqual([ - { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } }, - { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } }, - { - meta: { - index: 'apm-*', - key: 'service.name', - params: ['elastic-co', 'kibana-front'], - type: 'phrases', - value: 'elastic-co, kibana-front', - }, - query: { - bool: { - minimum_should_match: 1, - should: [ - { - match_phrase: { - 'service.name': 'elastic-co', - }, - }, - { - match_phrase: { - 'service.name': 'kibana-front', - }, - }, - ], - }, - }, - }, - { - meta: { - index: 'apm-*', - }, - query: { - match_phrase: { - 'user_agent.name': 'Firefox', - }, - }, - }, - { - meta: { - index: 'apm-*', - negate: true, - }, - query: { - match_phrase: { - 'user_agent.name': 'Chrome', - }, - }, - }, - ]); - }); - }); + lnsAttr = new LensAttributes([layerConfig1]); - describe('Layer breakdowns', function () { - it('should add breakdown column', function () { - lnsAttr.addBreakdown(USER_AGENT_NAME); + lnsAttr.getBreakdownColumn({ + sourceField: USER_AGENT_NAME, + layerId: 'layer0', + indexPattern: mockIndexPattern, + }); expect(lnsAttr.visualization.layers).toEqual([ { - accessors: ['y-axis-column'], - layerId: 'layer1', + accessors: ['y-axis-column-layer0'], + layerId: 'layer0', palette: undefined, seriesType: 'line', - splitAccessor: 'break-down-column', - xAccessor: 'x-axis-column', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }], + splitAccessor: 'breakdown-column-layer0', + xAccessor: 'x-axis-column-layer0', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], }, ]); - expect(lnsAttr.layers.layer1).toEqual({ - columnOrder: ['x-axis-column', 'break-down-column', 'y-axis-column'], + expect(lnsAttr.layers.layer0).toEqual({ + columnOrder: ['x-axis-column-layer0', 'breakdown-column-layer0', 'y-axis-column-layer0'], columns: { - 'break-down-column': { + 'breakdown-column-layer0': { dataType: 'string', isBucketed: true, label: 'Top values of Browser family', operationType: 'terms', params: { missingBucket: false, - orderBy: { columnId: 'y-axis-column', type: 'column' }, + orderBy: { + columnId: 'y-axis-column-layer0', + type: 'column', + }, orderDirection: 'desc', otherBucket: true, size: 10, @@ -349,10 +319,10 @@ describe('Lens Attribute', () => { scale: 'ordinal', sourceField: 'user_agent.name', }, - 'x-axis-column': { + 'x-axis-column-layer0': { dataType: 'number', isBucketed: true, - label: 'Page load time', + label: 'Largest contentful paint', operationType: 'range', params: { maxBars: 'auto', @@ -360,62 +330,47 @@ describe('Lens Attribute', () => { type: 'histogram', }, scale: 'interval', - sourceField: 'transaction.duration.us', + sourceField: 'transaction.marks.agent.largestContentfulPaint', }, - 'y-axis-column': { + 'y-axis-column-layer0': { dataType: 'number', isBucketed: false, label: 'Pages loaded', operationType: 'count', scale: 'ratio', sourceField: 'Records', + filter: { + language: 'kuery', + query: + 'transaction.type: page-load and processor.event: transaction and transaction.type : *', + }, }, }, incompleteColumns: {}, }); }); + }); - it('should remove breakdown column', function () { - lnsAttr.addBreakdown(USER_AGENT_NAME); - - lnsAttr.removeBreakdown(); + describe('Layer Filters', function () { + it('should return expected filters', function () { + reportViewConfig.filters?.push( + ...buildPhrasesFilter('service.name', ['elastic', 'kibana'], mockIndexPattern) + ); - expect(lnsAttr.visualization.layers).toEqual([ - { - accessors: ['y-axis-column'], - layerId: 'layer1', - palette: undefined, - seriesType: 'line', - xAccessor: 'x-axis-column', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }], - }, - ]); + const layerConfig1: LayerConfig = { + reportConfig: reportViewConfig, + seriesType: 'line', + operationType: 'count', + indexPattern: mockIndexPattern, + reportDefinitions: { 'performance.metric': [LCP_FIELD] }, + time: { from: 'now-15m', to: 'now' }, + }; - expect(lnsAttr.layers.layer1.columnOrder).toEqual(['x-axis-column', 'y-axis-column']); + const filters = lnsAttr.getLayerFilters(layerConfig1, 2); - expect(lnsAttr.layers.layer1.columns).toEqual({ - 'x-axis-column': { - dataType: 'number', - isBucketed: true, - label: 'Page load time', - operationType: 'range', - params: { - maxBars: 'auto', - ranges: [{ from: 0, label: '', to: 1000 }], - type: 'histogram', - }, - scale: 'interval', - sourceField: 'transaction.duration.us', - }, - 'y-axis-column': { - dataType: 'number', - isBucketed: false, - label: 'Pages loaded', - operationType: 'count', - scale: 'ratio', - sourceField: 'Records', - }, - }); + expect(filters).toEqual( + '@timestamp >= now-15m and @timestamp <= now and transaction.type: page-load and processor.event: transaction and transaction.type : * and service.name: (elastic or kibana)' + ); }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 22ad18c663b32..208e8d8ba43c2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -27,13 +27,12 @@ import { TermsIndexPatternColumn, CardinalityIndexPatternColumn, } from '../../../../../../lens/public'; -import { - buildPhraseFilter, - buildPhrasesFilter, - IndexPattern, -} from '../../../../../../../../src/plugins/data/common'; +import { urlFiltersToKueryString } from '../utils/stringify_kueries'; +import { ExistsFilter, IndexPattern } from '../../../../../../../../src/plugins/data/common'; import { FieldLabels, FILTER_RECORDS, USE_BREAK_DOWN_COLUMN, TERMS_COLUMN } from './constants'; import { ColumnFilter, DataSeries, UrlFilter, URLReportDefinition } from '../types'; +import { PersistableFilter } from '../../../../../../lens/common'; +import { parseAbsoluteDate } from '../series_date_picker/date_range_picker'; function getLayerReferenceName(layerId: string) { return `indexpattern-datasource-layer-${layerId}`; @@ -87,46 +86,50 @@ export const parseCustomFieldName = ( return { fieldName, columnType, columnFilters, timeScale, columnLabel }; }; -export class LensAttributes { +export interface LayerConfig { + filters?: UrlFilter[]; + reportConfig: DataSeries; + breakdown?: string; + seriesType?: SeriesType; + operationType?: OperationType; + reportDefinitions: URLReportDefinition; + time: { to: string; from: string }; indexPattern: IndexPattern; +} + +export class LensAttributes { layers: Record; visualization: XYState; - filters: UrlFilter[]; - seriesType: SeriesType; - reportViewConfig: DataSeries; - reportDefinitions: URLReportDefinition; - breakdownSource?: string; + layerConfigs: LayerConfig[]; - constructor( - indexPattern: IndexPattern, - reportViewConfig: DataSeries, - seriesType?: SeriesType, - filters?: UrlFilter[], - operationType?: OperationType, - reportDefinitions?: URLReportDefinition, - breakdownSource?: string - ) { - this.indexPattern = indexPattern; + constructor(layerConfigs: LayerConfig[]) { this.layers = {}; - this.filters = filters ?? []; - this.reportDefinitions = reportDefinitions ?? {}; - this.breakdownSource = breakdownSource; - - if (operationType) { - reportViewConfig.yAxisColumns.forEach((yAxisColumn) => { - if (typeof yAxisColumn.operationType !== undefined) { - yAxisColumn.operationType = operationType as FieldBasedIndexPatternColumn['operationType']; - } - }); - } - this.seriesType = seriesType ?? reportViewConfig.defaultSeriesType; - this.reportViewConfig = reportViewConfig; - this.layers.layer1 = this.getLayer(); + + layerConfigs.forEach(({ reportConfig, operationType }) => { + if (operationType) { + reportConfig.yAxisColumns.forEach((yAxisColumn) => { + if (typeof yAxisColumn.operationType !== undefined) { + yAxisColumn.operationType = operationType as FieldBasedIndexPatternColumn['operationType']; + } + }); + } + }); + + this.layerConfigs = layerConfigs; + this.layers = this.getLayers(); this.visualization = this.getXyState(); } - getBreakdownColumn(sourceField: string): TermsIndexPatternColumn { - const fieldMeta = this.indexPattern.getFieldByName(sourceField); + getBreakdownColumn({ + sourceField, + layerId, + indexPattern, + }: { + sourceField: string; + layerId: string; + indexPattern: IndexPattern; + }): TermsIndexPatternColumn { + const fieldMeta = indexPattern.getFieldByName(sourceField); return { sourceField, @@ -136,8 +139,8 @@ export class LensAttributes { scale: 'ordinal', isBucketed: true, params: { + orderBy: { type: 'column', columnId: `y-axis-column-${layerId}` }, size: 10, - orderBy: { type: 'column', columnId: 'y-axis-column' }, orderDirection: 'desc', otherBucket: true, missingBucket: false, @@ -145,36 +148,14 @@ export class LensAttributes { }; } - addBreakdown(sourceField: string) { - const { xAxisColumn } = this.reportViewConfig; - if (xAxisColumn?.sourceField === USE_BREAK_DOWN_COLUMN) { - // do nothing since this will be used a x axis source - return; - } - this.layers.layer1.columns['break-down-column'] = this.getBreakdownColumn(sourceField); - - this.layers.layer1.columnOrder = [ - 'x-axis-column', - 'break-down-column', - 'y-axis-column', - ...Object.keys(this.getChildYAxises()), - ]; - - this.visualization.layers[0].splitAccessor = 'break-down-column'; - } - - removeBreakdown() { - delete this.layers.layer1.columns['break-down-column']; - - this.layers.layer1.columnOrder = ['x-axis-column', 'y-axis-column']; - - this.visualization.layers[0].splitAccessor = undefined; - } - - getNumberRangeColumn(sourceField: string, label?: string): RangeIndexPatternColumn { + getNumberRangeColumn( + sourceField: string, + reportViewConfig: DataSeries, + label?: string + ): RangeIndexPatternColumn { return { sourceField, - label: this.reportViewConfig.labels[sourceField] ?? label, + label: reportViewConfig.labels[sourceField] ?? label, dataType: 'number', operationType: 'range', isBucketed: true, @@ -187,16 +168,36 @@ export class LensAttributes { }; } - getCardinalityColumn(sourceField: string, label?: string) { - return this.getNumberOperationColumn(sourceField, 'unique_count', label); + getCardinalityColumn({ + sourceField, + label, + reportViewConfig, + }: { + sourceField: string; + label?: string; + reportViewConfig: DataSeries; + }) { + return this.getNumberOperationColumn({ + sourceField, + operationType: 'unique_count', + label, + reportViewConfig, + }); } - getNumberColumn( - sourceField: string, - columnType?: string, - operationType?: string, - label?: string - ) { + getNumberColumn({ + reportViewConfig, + label, + sourceField, + columnType, + operationType, + }: { + sourceField: string; + columnType?: string; + operationType?: string; + label?: string; + reportViewConfig: DataSeries; + }) { if (columnType === 'operation' || operationType) { if ( operationType === 'median' || @@ -204,48 +205,58 @@ export class LensAttributes { operationType === 'sum' || operationType === 'unique_count' ) { - return this.getNumberOperationColumn(sourceField, operationType, label); + return this.getNumberOperationColumn({ + sourceField, + operationType, + label, + reportViewConfig, + }); } if (operationType?.includes('th')) { - return this.getPercentileNumberColumn(sourceField, operationType); + return this.getPercentileNumberColumn(sourceField, operationType, reportViewConfig!); } } - return this.getNumberRangeColumn(sourceField, label); + return this.getNumberRangeColumn(sourceField, reportViewConfig!, label); } - getNumberOperationColumn( - sourceField: string, - operationType: 'average' | 'median' | 'sum' | 'unique_count', - label?: string - ): + getNumberOperationColumn({ + sourceField, + label, + reportViewConfig, + operationType, + }: { + sourceField: string; + operationType: 'average' | 'median' | 'sum' | 'unique_count'; + label?: string; + reportViewConfig: DataSeries; + }): | AvgIndexPatternColumn | MedianIndexPatternColumn | SumIndexPatternColumn | CardinalityIndexPatternColumn { return { ...buildNumberColumn(sourceField), - label: - label || - i18n.translate('xpack.observability.expView.columns.operation.label', { - defaultMessage: '{operationType} of {sourceField}', - values: { - sourceField: this.reportViewConfig.labels[sourceField], - operationType: capitalize(operationType), - }, - }), + label: i18n.translate('xpack.observability.expView.columns.operation.label', { + defaultMessage: '{operationType} of {sourceField}', + values: { + sourceField: label || reportViewConfig.labels[sourceField], + operationType: capitalize(operationType), + }, + }), operationType, }; } getPercentileNumberColumn( sourceField: string, - percentileValue: string + percentileValue: string, + reportViewConfig: DataSeries ): PercentileIndexPatternColumn { return { ...buildNumberColumn(sourceField), label: i18n.translate('xpack.observability.expView.columns.label', { defaultMessage: '{percentileValue} percentile of {sourceField}', - values: { sourceField: this.reportViewConfig.labels[sourceField], percentileValue }, + values: { sourceField: reportViewConfig.labels[sourceField], percentileValue }, }), operationType: 'percentile', params: { percentile: Number(percentileValue.split('th')[0]) }, @@ -268,7 +279,7 @@ export class LensAttributes { return { operationType: 'terms', sourceField, - label: label || 'Top values of ' + sourceField, + label: 'Top values of ' + label || sourceField, dataType: 'string', isBucketed: true, scale: 'ordinal', @@ -283,30 +294,45 @@ export class LensAttributes { }; } - getXAxis() { - const { xAxisColumn } = this.reportViewConfig; + getXAxis(layerConfig: LayerConfig, layerId: string) { + const { xAxisColumn } = layerConfig.reportConfig; if (xAxisColumn?.sourceField === USE_BREAK_DOWN_COLUMN) { - return this.getBreakdownColumn(this.breakdownSource || this.reportViewConfig.breakdowns[0]); + return this.getBreakdownColumn({ + layerId, + indexPattern: layerConfig.indexPattern, + sourceField: layerConfig.breakdown || layerConfig.reportConfig.breakdowns[0], + }); } - return this.getColumnBasedOnType(xAxisColumn.sourceField!, undefined, xAxisColumn.label); + return this.getColumnBasedOnType({ + layerConfig, + label: xAxisColumn.label, + sourceField: xAxisColumn.sourceField!, + }); } - getColumnBasedOnType( - sourceField: string, - operationType?: OperationType, - label?: string, - colIndex?: number - ) { + getColumnBasedOnType({ + sourceField, + label, + layerConfig, + operationType, + colIndex, + }: { + sourceField: string; + operationType?: OperationType; + label?: string; + layerConfig: LayerConfig; + colIndex?: number; + }) { const { fieldMeta, columnType, fieldName, - columnFilters, - timeScale, columnLabel, - } = this.getFieldMeta(sourceField); + timeScale, + columnFilters, + } = this.getFieldMeta(sourceField, layerConfig); const { type: fieldType } = fieldMeta ?? {}; if (columnType === TERMS_COLUMN) { @@ -325,47 +351,76 @@ export class LensAttributes { return this.getDateHistogramColumn(fieldName); } if (fieldType === 'number') { - return this.getNumberColumn(fieldName, columnType, operationType, columnLabel || label); + return this.getNumberColumn({ + sourceField: fieldName, + columnType, + operationType, + label: columnLabel || label, + reportViewConfig: layerConfig.reportConfig, + }); } if (operationType === 'unique_count') { - return this.getCardinalityColumn(fieldName, columnLabel || label); + return this.getCardinalityColumn({ + sourceField: fieldName, + label: columnLabel || label, + reportViewConfig: layerConfig.reportConfig, + }); } // FIXME review my approach again return this.getDateHistogramColumn(fieldName); } - getCustomFieldName(sourceField: string) { - return parseCustomFieldName(sourceField, this.reportViewConfig, this.reportDefinitions); + getCustomFieldName({ + sourceField, + layerConfig, + }: { + sourceField: string; + layerConfig: LayerConfig; + }) { + return parseCustomFieldName( + sourceField, + layerConfig.reportConfig, + layerConfig.reportDefinitions + ); } - getFieldMeta(sourceField: string) { + getFieldMeta(sourceField: string, layerConfig: LayerConfig) { const { fieldName, columnType, + columnLabel, columnFilters, timeScale, - columnLabel, - } = this.getCustomFieldName(sourceField); + } = this.getCustomFieldName({ + sourceField, + layerConfig, + }); - const fieldMeta = this.indexPattern.getFieldByName(fieldName); + const fieldMeta = layerConfig.indexPattern.getFieldByName(fieldName); - return { fieldMeta, fieldName, columnType, columnFilters, timeScale, columnLabel }; + return { fieldMeta, fieldName, columnType, columnLabel, columnFilters, timeScale }; } - getMainYAxis() { - const { sourceField, operationType, label } = this.reportViewConfig.yAxisColumns[0]; + getMainYAxis(layerConfig: LayerConfig) { + const { sourceField, operationType, label } = layerConfig.reportConfig.yAxisColumns[0]; if (sourceField === 'Records' || !sourceField) { return this.getRecordsColumn(label); } - return this.getColumnBasedOnType(sourceField!, operationType, label, 0); + return this.getColumnBasedOnType({ + sourceField, + operationType, + label, + layerConfig, + colIndex: 0, + }); } - getChildYAxises() { + getChildYAxises(layerConfig: LayerConfig) { const lensColumns: Record = {}; - const yAxisColumns = this.reportViewConfig.yAxisColumns; + const yAxisColumns = layerConfig.reportConfig.yAxisColumns; // 1 means there is only main y axis if (yAxisColumns.length === 1) { return lensColumns; @@ -373,12 +428,13 @@ export class LensAttributes { for (let i = 1; i < yAxisColumns.length; i++) { const { sourceField, operationType, label } = yAxisColumns[i]; - lensColumns[`y-axis-column-${i}`] = this.getColumnBasedOnType( - sourceField!, + lensColumns[`y-axis-column-${i}`] = this.getColumnBasedOnType({ + sourceField: sourceField!, operationType, label, - i - ); + layerConfig, + colIndex: i, + }); } return lensColumns; } @@ -396,20 +452,139 @@ export class LensAttributes { scale: 'ratio', sourceField: 'Records', filter: columnFilter, - timeScale, + ...(timeScale ? { timeScale } : {}), } as CountIndexPatternColumn; } - getLayer() { - return { - columnOrder: ['x-axis-column', 'y-axis-column', ...Object.keys(this.getChildYAxises())], - columns: { - 'x-axis-column': this.getXAxis(), - 'y-axis-column': this.getMainYAxis(), - ...this.getChildYAxises(), - }, - incompleteColumns: {}, - }; + getLayerFilters(layerConfig: LayerConfig, totalLayers: number) { + const { + filters, + time: { from, to }, + reportConfig: { filters: layerFilters, reportType }, + } = layerConfig; + let baseFilters = ''; + if (reportType !== 'kpi-over-time' && totalLayers > 1) { + // for kpi over time, we don't need to add time range filters + // since those are essentially plotted along the x-axis + baseFilters += `@timestamp >= ${from} and @timestamp <= ${to}`; + } + + layerFilters?.forEach((filter: PersistableFilter | ExistsFilter) => { + const qFilter = filter as PersistableFilter; + if (qFilter.query?.match_phrase) { + const fieldName = Object.keys(qFilter.query.match_phrase)[0]; + const kql = `${fieldName}: ${qFilter.query.match_phrase[fieldName]}`; + if (baseFilters.length > 0) { + baseFilters += ` and ${kql}`; + } else { + baseFilters += kql; + } + } + if (qFilter.query?.bool?.should) { + const values: string[] = []; + let fieldName = ''; + qFilter.query?.bool.should.forEach((ft: PersistableFilter['query']['match_phrase']) => { + if (ft.match_phrase) { + fieldName = Object.keys(ft.match_phrase)[0]; + values.push(ft.match_phrase[fieldName]); + } + }); + + const kueryString = `${fieldName}: (${values.join(' or ')})`; + + if (baseFilters.length > 0) { + baseFilters += ` and ${kueryString}`; + } else { + baseFilters += kueryString; + } + } + const existFilter = filter as ExistsFilter; + + if (existFilter.exists) { + const fieldName = existFilter.exists.field; + const kql = `${fieldName} : *`; + if (baseFilters.length > 0) { + baseFilters += ` and ${kql}`; + } else { + baseFilters += kql; + } + } + }); + + const rFilters = urlFiltersToKueryString(filters ?? []); + if (!baseFilters) { + return rFilters; + } + if (!rFilters) { + return baseFilters; + } + return `${rFilters} and ${baseFilters}`; + } + + getTimeShift(mainLayerConfig: LayerConfig, layerConfig: LayerConfig, index: number) { + if (index === 0 || mainLayerConfig.reportConfig.reportType !== 'kpi-over-time') { + return null; + } + + const { + time: { from: mainFrom }, + } = mainLayerConfig; + + const { + time: { from }, + } = layerConfig; + + const inDays = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days'); + if (inDays > 1) { + return inDays + 'd'; + } + const inHours = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours'); + return inHours + 'h'; + } + + getLayers() { + const layers: Record = {}; + const layerConfigs = this.layerConfigs; + + layerConfigs.forEach((layerConfig, index) => { + const { breakdown } = layerConfig; + + const layerId = `layer${index}`; + const columnFilter = this.getLayerFilters(layerConfig, layerConfigs.length); + const timeShift = this.getTimeShift(this.layerConfigs[0], layerConfig, index); + const mainYAxis = this.getMainYAxis(layerConfig); + layers[layerId] = { + columnOrder: [ + `x-axis-column-${layerId}`, + ...(breakdown ? [`breakdown-column-${layerId}`] : []), + `y-axis-column-${layerId}`, + ...Object.keys(this.getChildYAxises(layerConfig)), + ], + columns: { + [`x-axis-column-${layerId}`]: this.getXAxis(layerConfig, layerId), + [`y-axis-column-${layerId}`]: { + ...mainYAxis, + label: timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label, + filter: { query: columnFilter, language: 'kuery' }, + ...(timeShift ? { timeShift } : {}), + }, + ...(breakdown && breakdown !== USE_BREAK_DOWN_COLUMN + ? // do nothing since this will be used a x axis source + { + [`breakdown-column-${layerId}`]: this.getBreakdownColumn({ + layerId, + sourceField: breakdown, + indexPattern: layerConfig.indexPattern, + }), + } + : {}), + ...this.getChildYAxises(layerConfig), + }, + incompleteColumns: {}, + }; + }); + + return layers; } getXyState(): XYState { @@ -422,71 +597,48 @@ export class LensAttributes { tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, preferredSeriesType: 'line', - layers: [ - { - accessors: ['y-axis-column', ...Object.keys(this.getChildYAxises())], - layerId: 'layer1', - seriesType: this.seriesType ?? 'line', - palette: this.reportViewConfig.palette, - yConfig: this.reportViewConfig.yConfig || [ - { forAccessor: 'y-axis-column', color: 'green' }, - ], - xAccessor: 'x-axis-column', - }, - ], - ...(this.reportViewConfig.yTitle ? { yTitle: this.reportViewConfig.yTitle } : {}), + layers: this.layerConfigs.map((layerConfig, index) => ({ + accessors: [ + `y-axis-column-layer${index}`, + ...Object.keys(this.getChildYAxises(layerConfig)), + ], + layerId: `layer${index}`, + seriesType: layerConfig.seriesType || layerConfig.reportConfig.defaultSeriesType, + palette: layerConfig.reportConfig.palette, + yConfig: layerConfig.reportConfig.yConfig || [ + { forAccessor: `y-axis-column-layer${index}` }, + ], + xAccessor: `x-axis-column-layer${index}`, + ...(layerConfig.breakdown ? { splitAccessor: `breakdown-column-layer${index}` } : {}), + })), + ...(this.layerConfigs[0].reportConfig.yTitle + ? { yTitle: this.layerConfigs[0].reportConfig.yTitle } + : {}), }; } - parseFilters() { - const defaultFilters = this.reportViewConfig.filters ?? []; - const parsedFilters = this.reportViewConfig.filters ? [...defaultFilters] : []; - - this.filters.forEach(({ field, values = [], notValues = [] }) => { - const fieldMeta = this.indexPattern.fields.find((fieldT) => fieldT.name === field)!; - - if (values?.length > 0) { - if (values?.length > 1) { - const multiFilter = buildPhrasesFilter(fieldMeta, values, this.indexPattern); - parsedFilters.push(multiFilter); - } else { - const filter = buildPhraseFilter(fieldMeta, values[0], this.indexPattern); - parsedFilters.push(filter); - } - } - - if (notValues?.length > 0) { - if (notValues?.length > 1) { - const multiFilter = buildPhrasesFilter(fieldMeta, notValues, this.indexPattern); - multiFilter.meta.negate = true; - parsedFilters.push(multiFilter); - } else { - const filter = buildPhraseFilter(fieldMeta, notValues[0], this.indexPattern); - filter.meta.negate = true; - parsedFilters.push(filter); - } - } - }); - - return parsedFilters; - } + parseFilters() {} getJSON(): TypedLensByValueInput['attributes'] { + const uniqueIndexPatternsIds = Array.from( + new Set([...this.layerConfigs.map(({ indexPattern }) => indexPattern.id)]) + ); + return { title: 'Prefilled from exploratory view app', description: '', visualizationType: 'lnsXY', references: [ - { - id: this.indexPattern.id!, + ...uniqueIndexPatternsIds.map((patternId) => ({ + id: patternId!, name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', - }, - { - id: this.indexPattern.id!, - name: getLayerReferenceName('layer1'), + })), + ...this.layerConfigs.map(({ indexPattern }, index) => ({ + id: indexPattern.id!, + name: getLayerReferenceName(`layer${index}`), type: 'index-pattern', - }, + })), ], state: { datasourceStates: { @@ -496,7 +648,7 @@ export class LensAttributes { }, visualization: this.visualization, query: { query: '', language: 'kuery' }, - filters: this.parseFilters(), + filters: [], }, }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts index 6f9806660e489..e1cb5a0370fb2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts @@ -14,7 +14,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): DataSeries { return { - reportType: 'mobile-device-distribution', + reportType: 'device-data-distribution', defaultSeriesType: 'bar', seriesTypes: ['bar', 'bar_horizontal'], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts index 854f844db047d..b958c0dd71528 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts @@ -10,10 +10,10 @@ import { FieldLabels, RECORDS_FIELD } from '../constants'; import { buildExistsFilter } from '../utils'; import { MONITORS_DURATION_LABEL, PINGS_LABEL } from '../constants/labels'; -export function getSyntheticsDistributionConfig({ indexPattern }: ConfigProps): DataSeries { +export function getSyntheticsDistributionConfig({ series, indexPattern }: ConfigProps): DataSeries { return { reportType: 'data-distribution', - defaultSeriesType: 'line', + defaultSeriesType: series?.seriesType || 'line', seriesTypes: [], xAxisColumn: { sourceField: 'performance.metric', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts index 9b299e7d70bcc..edf2a42415820 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts @@ -10,16 +10,16 @@ export const sampleAttribute = { visualizationType: 'lnsXY', references: [ { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' }, - { id: 'apm-*', name: 'indexpattern-datasource-layer-layer1', type: 'index-pattern' }, + { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' }, ], state: { datasourceStates: { indexpattern: { layers: { - layer1: { - columnOrder: ['x-axis-column', 'y-axis-column'], + layer0: { + columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'], columns: { - 'x-axis-column': { + 'x-axis-column-layer0': { sourceField: 'transaction.duration.us', label: 'Page load time', dataType: 'number', @@ -32,13 +32,18 @@ export const sampleAttribute = { maxBars: 'auto', }, }, - 'y-axis-column': { + 'y-axis-column-layer0': { dataType: 'number', isBucketed: false, label: 'Pages loaded', operationType: 'count', scale: 'ratio', sourceField: 'Records', + filter: { + language: 'kuery', + query: + 'transaction.type: page-load and processor.event: transaction and transaction.type : *', + }, }, }, incompleteColumns: {}, @@ -57,18 +62,15 @@ export const sampleAttribute = { preferredSeriesType: 'line', layers: [ { - accessors: ['y-axis-column'], - layerId: 'layer1', + accessors: ['y-axis-column-layer0'], + layerId: 'layer0', seriesType: 'line', - yConfig: [{ forAccessor: 'y-axis-column', color: 'green' }], - xAccessor: 'x-axis-column', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], + xAccessor: 'x-axis-column-layer0', }, ], }, query: { query: '', language: 'kuery' }, - filters: [ - { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } }, - { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } }, - ], + filters: [], }, }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts index fc60800bc4403..9b1e7ec141ca2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts @@ -5,11 +5,12 @@ * 2.0. */ import rison, { RisonValue } from 'rison-node'; +import type { SeriesUrl, UrlFilter } from '../types'; import type { AllSeries, AllShortSeries } from '../hooks/use_series_storage'; -import type { SeriesUrl } from '../types'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; -import { esFilters } from '../../../../../../../../src/plugins/data/public'; +import { esFilters, ExistsFilter } from '../../../../../../../../src/plugins/data/public'; import { URL_KEYS } from './constants/url_constants'; +import { PersistableFilter } from '../../../../../../lens/common'; export function convertToShortUrl(series: SeriesUrl) { const { @@ -51,7 +52,7 @@ export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') { } export function buildPhraseFilter(field: string, value: string, indexPattern: IIndexPattern) { - const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field); + const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field); if (fieldMeta) { return [esFilters.buildPhraseFilter(fieldMeta, value, indexPattern)]; } @@ -59,7 +60,7 @@ export function buildPhraseFilter(field: string, value: string, indexPattern: II } export function buildPhrasesFilter(field: string, value: string[], indexPattern: IIndexPattern) { - const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field); + const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field); if (fieldMeta) { return [esFilters.buildPhrasesFilter(fieldMeta, value, indexPattern)]; } @@ -67,9 +68,38 @@ export function buildPhrasesFilter(field: string, value: string[], indexPattern: } export function buildExistsFilter(field: string, indexPattern: IIndexPattern) { - const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field); + const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field); if (fieldMeta) { return [esFilters.buildExistsFilter(fieldMeta, indexPattern)]; } return []; } + +type FiltersType = PersistableFilter[] | ExistsFilter[]; + +export function urlFilterToPersistedFilter({ + urlFilters, + initFilters, + indexPattern, +}: { + urlFilters: UrlFilter[]; + initFilters: FiltersType; + indexPattern: IIndexPattern; +}) { + const parsedFilters: FiltersType = initFilters ? [...initFilters] : []; + + urlFilters.forEach(({ field, values = [], notValues = [] }) => { + if (values?.length > 0) { + const filter = buildPhrasesFilter(field, values, indexPattern); + parsedFilters.push(...filter); + } + + if (notValues?.length > 0) { + const filter = buildPhrasesFilter(field, notValues, indexPattern)[0]; + filter.meta.negate = true; + parsedFilters.push(filter); + } + }); + + return parsedFilters; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx index 779049601bd6d..989ebf17c2062 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -51,8 +51,9 @@ describe('ExploratoryView', () => { const initSeries = { data: { 'ux-series': { + isNew: true, dataType: 'ux' as const, - reportType: 'dist' as const, + reportType: 'data-distribution' as const, breakdown: 'user_agent .name', reportDefinitions: { 'service.name': ['elastic-co'] }, time: { from: 'now-15m', to: 'now' }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx index 329ed20ffed3d..ad85ecab968b2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -5,9 +5,10 @@ * 2.0. */ import { i18n } from '@kbn/i18n'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState, useCallback } from 'react'; import { EuiPanel, EuiTitle } from '@elastic/eui'; import styled from 'styled-components'; +import { isEmpty } from 'lodash'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { ExploratoryViewHeader } from './header/header'; @@ -17,10 +18,37 @@ import { EmptyView } from './components/empty_view'; import { TypedLensByValueInput } from '../../../../../lens/public'; import { useAppIndexPatternContext } from './hooks/use_app_index_pattern'; import { SeriesBuilder } from './series_builder/series_builder'; +import { SeriesUrl } from './types'; + +export const combineTimeRanges = ( + allSeries: Record, + firstSeries?: SeriesUrl +) => { + let to: string = ''; + let from: string = ''; + if (firstSeries?.reportType === 'kpi-over-time') { + return firstSeries.time; + } + Object.values(allSeries ?? {}).forEach((series) => { + if (series.dataType && series.reportType && !isEmpty(series.reportDefinitions)) { + const seriesTo = new Date(series.time.to); + const seriesFrom = new Date(series.time.from); + if (!to || seriesTo > new Date(to)) { + to = series.time.to; + } + if (!from || seriesFrom < new Date(from)) { + from = series.time.from; + } + } + }); + return { to, from }; +}; export function ExploratoryView({ saveAttributes, + multiSeries, }: { + multiSeries?: boolean; saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void; }) { const { @@ -33,6 +61,8 @@ export function ExploratoryView({ const [height, setHeight] = useState('100vh'); const [seriesId, setSeriesId] = useState(''); + const [lastUpdated, setLastUpdated] = useState(); + const [lensAttributes, setLensAttributes] = useState( null ); @@ -47,9 +77,7 @@ export function ExploratoryView({ setSeriesId(firstSeriesId); }, [allSeries, firstSeriesId]); - const lensAttributesT = useLensAttributes({ - seriesId, - }); + const lensAttributesT = useLensAttributes(); const setHeightOffset = () => { if (seriesBuilderRef?.current && wrapperRef.current) { @@ -60,10 +88,12 @@ export function ExploratoryView({ }; useEffect(() => { - if (series?.dataType) { - loadIndexPattern({ dataType: series?.dataType }); - } - }, [series?.dataType, loadIndexPattern]); + Object.values(allSeries).forEach((seriesT) => { + loadIndexPattern({ + dataType: seriesT.dataType, + }); + }); + }, [allSeries, loadIndexPattern]); useEffect(() => { setLensAttributes(lensAttributesT); @@ -72,47 +102,62 @@ export function ExploratoryView({ } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(lensAttributesT ?? {}), series?.reportType, series?.time?.from]); + }, [JSON.stringify(lensAttributesT ?? {})]); useEffect(() => { setHeightOffset(); }); + const timeRange = combineTimeRanges(allSeries, series); + + const onLensLoad = useCallback(() => { + setLastUpdated(Date.now()); + }, []); + + const onBrushEnd = useCallback( + ({ range }: { range: number[] }) => { + if (series?.reportType !== 'data-distribution') { + setSeries(seriesId, { + ...series, + time: { + from: new Date(range[0]).toISOString(), + to: new Date(range[1]).toISOString(), + }, + }); + } else { + notifications?.toasts.add( + i18n.translate('xpack.observability.exploratoryView.noBrusing', { + defaultMessage: 'Zoom by brush selection is only available on time series charts.', + }) + ); + } + }, + [notifications?.toasts, series, seriesId, setSeries] + ); + return ( {lens ? ( <> - {lensAttributes && seriesId && series?.reportType && series?.time ? ( + {lensAttributes && timeRange.to && timeRange.from ? ( { - if (series?.reportType !== 'dist') { - setSeries(seriesId, { - ...series, - time: { - from: new Date(range[0]).toISOString(), - to: new Date(range[1]).toISOString(), - }, - }); - } else { - notifications?.toasts.add( - i18n.translate('xpack.observability.exploratoryView.noBrusing', { - defaultMessage: - 'Zoom by brush selection is only available on time series charts.', - }) - ); - } - }} + onLoad={onLensLoad} + onBrushEnd={onBrushEnd} /> ) : ( )} - + ) : ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx index 1dedc4142f174..8cd8977fcf741 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx @@ -26,7 +26,7 @@ describe('ExploratoryViewHeader', function () { data: { 'uptime-pings-histogram': { dataType: 'synthetics' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, breakdown: 'monitor.status', time: { from: 'now-15m', to: 'now' }, }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx index 3e02207e26272..dbe9cd163451d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx @@ -13,6 +13,7 @@ import { useKibana } from '../../../../../../../../src/plugins/kibana_react/publ import { DataViewLabels } from '../configurations/constants'; import { ObservabilityAppServices } from '../../../../application/types'; import { useSeriesStorage } from '../hooks/use_series_storage'; +import { combineTimeRanges } from '../exploratory_view'; interface Props { seriesId: string; @@ -24,7 +25,7 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { const { lens } = kServices; - const { getSeries } = useSeriesStorage(); + const { getSeries, allSeries } = useSeriesStorage(); const series = getSeries(seriesId); @@ -32,6 +33,8 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { const LensSaveModalComponent = lens.SaveModalComponent; + const timeRange = combineTimeRanges(allSeries, series); + return ( <> @@ -63,7 +66,7 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { lens.navigateToPrefilledEditor( { id: '', - timeRange: series.time, + timeRange, attributes: lensAttributes, }, true diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx index 4259bb778e511..7a5f12a72b1f0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx @@ -15,7 +15,6 @@ import { getDataHandler } from '../../../../data_handler'; export interface IIndexPatternContext { loading: boolean; - selectedApp: AppDataType; indexPatterns: IndexPatternState; hasAppData: HasAppDataState; loadIndexPattern: (params: { dataType: AppDataType }) => void; @@ -29,10 +28,10 @@ interface ProviderProps { type HasAppDataState = Record; type IndexPatternState = Record; +type LoadingState = Record; export function IndexPatternContextProvider({ children }: ProviderProps) { - const [loading, setLoading] = useState(false); - const [selectedApp, setSelectedApp] = useState(); + const [loading, setLoading] = useState({} as LoadingState); const [indexPatterns, setIndexPatterns] = useState({} as IndexPatternState); const [hasAppData, setHasAppData] = useState({ infra_metrics: null, @@ -49,10 +48,9 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { const loadIndexPattern: IIndexPatternContext['loadIndexPattern'] = useCallback( async ({ dataType }) => { - setSelectedApp(dataType); + if (hasAppData[dataType] === null && !loading[dataType]) { + setLoading((prevState) => ({ ...prevState, [dataType]: true })); - if (hasAppData[dataType] === null) { - setLoading(true); try { let hasDataT = false; let indices: string | undefined = ''; @@ -78,23 +76,22 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { setIndexPatterns((prevState) => ({ ...prevState, [dataType]: indPattern })); } - setLoading(false); + setLoading((prevState) => ({ ...prevState, [dataType]: false })); } catch (e) { - setLoading(false); + setLoading((prevState) => ({ ...prevState, [dataType]: false })); } } }, - [data, hasAppData] + [data, hasAppData, loading] ); return ( loadingT), }} > {children} @@ -102,19 +99,23 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { ); } -export const useAppIndexPatternContext = () => { - const { selectedApp, loading, hasAppData, loadIndexPattern, indexPatterns } = useContext( +export const useAppIndexPatternContext = (dataType?: AppDataType) => { + const { loading, hasAppData, loadIndexPattern, indexPatterns } = useContext( (IndexPatternContext as unknown) as Context ); + if (dataType && !indexPatterns?.[dataType] && !loading) { + loadIndexPattern({ dataType }); + } + return useMemo(() => { return { hasAppData, - selectedApp, loading, - indexPattern: indexPatterns?.[selectedApp], - hasData: hasAppData?.[selectedApp], + indexPatterns, + indexPattern: dataType ? indexPatterns?.[dataType] : undefined, + hasData: dataType ? hasAppData?.[dataType] : undefined, loadIndexPattern, }; - }, [hasAppData, indexPatterns, loadIndexPattern, loading, selectedApp]); + }, [dataType, hasAppData, indexPatterns, loadIndexPattern, loading]); }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index 1c85bc5089b2a..11487afe28e96 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -8,17 +8,13 @@ import { useMemo } from 'react'; import { isEmpty } from 'lodash'; import { TypedLensByValueInput } from '../../../../../../lens/public'; -import { LensAttributes } from '../configurations/lens_attributes'; +import { LayerConfig, LensAttributes } from '../configurations/lens_attributes'; import { useSeriesStorage } from './use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; import { DataSeries, SeriesUrl, UrlFilter } from '../types'; import { useAppIndexPatternContext } from './use_app_index_pattern'; -interface Props { - seriesId: string; -} - export const getFiltersFromDefs = ( reportDefinitions: SeriesUrl['reportDefinitions'], dataViewConfig: DataSeries @@ -37,54 +33,51 @@ export const getFiltersFromDefs = ( }); }; -export const useLensAttributes = ({ - seriesId, -}: Props): TypedLensByValueInput['attributes'] | null => { - const { getSeries } = useSeriesStorage(); - const series = getSeries(seriesId); - const { breakdown, seriesType, operationType, reportType, dataType, reportDefinitions = {} } = - series ?? {}; +export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null => { + const { allSeriesIds, allSeries } = useSeriesStorage(); - const { indexPattern } = useAppIndexPatternContext(); + const { indexPatterns } = useAppIndexPatternContext(); return useMemo(() => { - if (!indexPattern || !reportType || isEmpty(reportDefinitions)) { + if (isEmpty(indexPatterns) || isEmpty(allSeriesIds)) { return null; } - const dataViewConfig = getDefaultConfigs({ - reportType, - dataType, - indexPattern, - }); + const layerConfigs: LayerConfig[] = []; + + allSeriesIds.forEach((seriesIdT) => { + const seriesT = allSeries[seriesIdT]; + const indexPattern = indexPatterns?.[seriesT?.dataType]; + if (indexPattern && seriesT.reportType && !isEmpty(seriesT.reportDefinitions)) { + const reportViewConfig = getDefaultConfigs({ + reportType: seriesT.reportType, + dataType: seriesT.dataType, + indexPattern, + }); - const filters: UrlFilter[] = (series.filters ?? []).concat( - getFiltersFromDefs(reportDefinitions, dataViewConfig) - ); + const filters: UrlFilter[] = (seriesT.filters ?? []).concat( + getFiltersFromDefs(seriesT.reportDefinitions, reportViewConfig) + ); - const lensAttributes = new LensAttributes( - indexPattern, - dataViewConfig, - seriesType, - filters, - operationType, - reportDefinitions, - breakdown - ); + layerConfigs.push({ + filters, + indexPattern, + reportConfig: reportViewConfig, + breakdown: seriesT.breakdown, + operationType: seriesT.operationType, + seriesType: seriesT.seriesType, + reportDefinitions: seriesT.reportDefinitions ?? {}, + time: seriesT.time, + }); + } + }); - if (breakdown) { - lensAttributes.addBreakdown(breakdown); + if (layerConfigs.length < 1) { + return null; } + const lensAttributes = new LensAttributes(layerConfigs); + return lensAttributes.getJSON(); - }, [ - indexPattern, - reportType, - reportDefinitions, - dataType, - series.filters, - seriesType, - operationType, - breakdown, - ]); + }, [indexPatterns, allSeriesIds, allSeries]); }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx index fac75f910a93f..e9ae43950d47d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx @@ -12,7 +12,7 @@ import { } from '../../../../../../../../src/plugins/kibana_utils/public'; import type { AppDataType, - ReportViewTypeId, + ReportViewType, SeriesUrl, UrlFilter, URLReportDefinition, @@ -36,6 +36,16 @@ interface ProviderProps { storage: IKbnUrlStateStorage | ISessionStorageStateStorage; } +function convertAllShortSeries(allShortSeries: AllShortSeries) { + const allSeriesIds = Object.keys(allShortSeries); + const allSeriesN: AllSeries = {}; + allSeriesIds.forEach((seriesKey) => { + allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]); + }); + + return allSeriesN; +} + export function UrlStorageContextProvider({ children, storage, @@ -45,15 +55,14 @@ export function UrlStorageContextProvider({ const [allShortSeries, setAllShortSeries] = useState( () => storage.get(allSeriesKey) ?? {} ); - const [allSeries, setAllSeries] = useState({}); + const [allSeries, setAllSeries] = useState(() => + convertAllShortSeries(storage.get(allSeriesKey) ?? {}) + ); const [firstSeriesId, setFirstSeriesId] = useState(''); useEffect(() => { const allSeriesIds = Object.keys(allShortSeries); - const allSeriesN: AllSeries = {}; - allSeriesIds.forEach((seriesKey) => { - allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]); - }); + const allSeriesN: AllSeries = convertAllShortSeries(allShortSeries ?? {}); setAllSeries(allSeriesN); setFirstSeriesId(allSeriesIds?.[0]); @@ -68,8 +77,10 @@ export function UrlStorageContextProvider({ }; const removeSeries = (seriesIdN: string) => { - delete allShortSeries[seriesIdN]; - delete allSeries[seriesIdN]; + setAllShortSeries((prevState) => { + delete prevState[seriesIdN]; + return { ...prevState }; + }); }; const allSeriesIds = Object.keys(allShortSeries); @@ -115,7 +126,7 @@ function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { interface ShortUrlSeries { [URL_KEYS.OPERATION_TYPE]?: OperationType; - [URL_KEYS.REPORT_TYPE]?: ReportViewTypeId; + [URL_KEYS.REPORT_TYPE]?: ReportViewType; [URL_KEYS.DATA_TYPE]?: AppDataType; [URL_KEYS.SERIES_TYPE]?: SeriesType; [URL_KEYS.BREAK_DOWN]?: string; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx index 3de29b02853e8..e55752ceb62ba 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx @@ -25,9 +25,11 @@ import { TypedLensByValueInput } from '../../../../../lens/public'; export function ExploratoryViewPage({ saveAttributes, + multiSeries = false, useSessionStorage = false, }: { useSessionStorage?: boolean; + multiSeries?: boolean; saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void; }) { useTrackPageview({ app: 'observability-overview', path: 'exploratory-view' }); @@ -59,7 +61,7 @@ export function ExploratoryViewPage({ - + diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx index 8e54ab7629d26..972e3beb4b722 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx @@ -35,8 +35,11 @@ import { getStubIndexPattern } from '../../../../../../../src/plugins/data/publi import indexPatternData from './configurations/test_data/test_index_pattern.json'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { setIndexPatterns } from '../../../../../../../src/plugins/data/public/services'; -import { IndexPatternsContract } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; -import { UrlFilter } from './types'; +import { + IndexPattern, + IndexPatternsContract, +} from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import { AppDataType, UrlFilter } from './types'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { ListItem } from '../../../hooks/use_values_list'; @@ -232,11 +235,11 @@ export const mockAppIndexPattern = () => { const loadIndexPattern = jest.fn(); const spy = jest.spyOn(useAppIndexPatternHook, 'useAppIndexPatternContext').mockReturnValue({ indexPattern: mockIndexPattern, - selectedApp: 'ux', hasData: true, loading: false, hasAppData: { ux: true } as any, loadIndexPattern, + indexPatterns: ({ ux: mockIndexPattern } as unknown) as Record, }); return { spy, loadIndexPattern }; }; @@ -260,7 +263,7 @@ function mockSeriesStorageContext({ }) { const mockDataSeries = data || { 'performance-distribution': { - reportType: 'dist', + reportType: 'data-distribution', dataType: 'ux', breakdown: breakdown || 'user_agent.name', time: { from: 'now-15m', to: 'now' }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx index 9ae8b68bf3e8c..50c2f91e6067d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx @@ -27,18 +27,14 @@ export function SeriesChartTypesSelect({ seriesTypes?: SeriesType[]; defaultChartType: SeriesType; }) { - const { getSeries, setSeries, allSeries } = useSeriesStorage(); + const { getSeries, setSeries } = useSeriesStorage(); const series = getSeries(seriesId); const seriesType = series?.seriesType ?? defaultChartType; const onChange = (value: SeriesType) => { - Object.keys(allSeries).forEach((seriesKey) => { - const seriesN = allSeries[seriesKey]; - - setSeries(seriesKey, { ...seriesN, seriesType: value }); - }); + setSeries(seriesId, { ...series, seriesType: value }); }; return ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx index e3c1666c533ef..b10702ebded57 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx @@ -29,7 +29,14 @@ describe('DataTypesCol', function () { fireEvent.click(screen.getByText(/user experience \(rum\)/i)); expect(setSeries).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'ux' }); + expect(setSeries).toHaveBeenCalledWith(seriesId, { + dataType: 'ux', + isNew: true, + time: { + from: 'now-15m', + to: 'now', + }, + }); }); it('should set series on change on already selected', function () { @@ -37,7 +44,7 @@ describe('DataTypesCol', function () { data: { [seriesId]: { dataType: 'synthetics' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, breakdown: 'monitor.status', time: { from: 'now-15m', to: 'now' }, }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx index 985afdf888868..f386f62d9ed73 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx @@ -31,7 +31,11 @@ export function DataTypesCol({ seriesId }: { seriesId: string }) { if (!dataType) { removeSeries(seriesId); } else { - setSeries(seriesId || `${dataType}-series`, { dataType } as any); + setSeries(seriesId || `${dataType}-series`, { + dataType, + isNew: true, + time: series.time, + } as any); } }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx index 175fbea9445c1..6be78084ae195 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx @@ -8,14 +8,23 @@ import React from 'react'; import styled from 'styled-components'; import { SeriesDatePicker } from '../../series_date_picker'; +import { DateRangePicker } from '../../series_date_picker/date_range_picker'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; interface Props { seriesId: string; } export function DatePickerCol({ seriesId }: Props) { + const { firstSeriesId, getSeries } = useSeriesStorage(); + const { reportType } = getSeries(firstSeriesId); + return ( - + {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? ( + + ) : ( + + )} ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx index c262a94f968be..516f04e3812ba 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx @@ -22,7 +22,7 @@ describe('OperationTypeSelect', function () { data: { 'performance-distribution': { dataType: 'ux' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, operationType: 'median' as const, time: { from: 'now-15m', to: 'now' }, }, @@ -39,7 +39,7 @@ describe('OperationTypeSelect', function () { data: { 'series-id': { dataType: 'ux' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, operationType: 'median' as const, time: { from: 'now-15m', to: 'now' }, }, @@ -53,7 +53,7 @@ describe('OperationTypeSelect', function () { expect(setSeries).toHaveBeenCalledWith('series-id', { operationType: 'median', dataType: 'ux', - reportType: 'kpi', + reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, }); @@ -61,7 +61,7 @@ describe('OperationTypeSelect', function () { expect(setSeries).toHaveBeenCalledWith('series-id', { operationType: '95th', dataType: 'ux', - reportType: 'kpi', + reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx index 805186e877d57..203382afc1624 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx @@ -15,7 +15,7 @@ import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fiel describe('Series Builder ReportBreakdowns', function () { const seriesId = 'test-series-id'; const dataViewSeries = getDefaultConfigs({ - reportType: 'dist', + reportType: 'data-distribution', dataType: 'ux', indexPattern: mockIndexPattern, }); @@ -45,7 +45,7 @@ describe('Series Builder ReportBreakdowns', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { breakdown: USER_AGENT_OS, dataType: 'ux', - reportType: 'dist', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }); }); @@ -67,7 +67,7 @@ describe('Series Builder ReportBreakdowns', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { breakdown: undefined, dataType: 'ux', - reportType: 'dist', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx index e947961fb4300..2e5c674b9fad8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx @@ -22,7 +22,7 @@ describe('Series Builder ReportDefinitionCol', function () { const seriesId = 'test-series-id'; const dataViewSeries = getDefaultConfigs({ - reportType: 'dist', + reportType: 'data-distribution', indexPattern: mockIndexPattern, dataType: 'ux', }); @@ -31,7 +31,7 @@ describe('Series Builder ReportDefinitionCol', function () { data: { [seriesId]: { dataType: 'ux' as const, - reportType: 'dist' as const, + reportType: 'data-distribution' as const, time: { from: 'now-30d', to: 'now' }, reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, }, @@ -81,7 +81,7 @@ describe('Series Builder ReportDefinitionCol', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'ux', reportDefinitions: {}, - reportType: 'dist', + reportType: 'data-distribution', time: { from: 'now-30d', to: 'now' }, }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx index 338f5d52c26fa..47962af0d4bc4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import styled from 'styled-components'; -import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { CustomReportField } from '../custom_report_field'; import { DataSeries, URLReportDefinition } from '../../types'; @@ -36,8 +35,6 @@ export function ReportDefinitionCol({ dataViewSeries: DataSeries; seriesId: string; }) { - const { indexPattern } = useAppIndexPatternContext(); - const { getSeries, setSeries } = useSeriesStorage(); const series = getSeries(seriesId); @@ -69,21 +66,20 @@ export function ReportDefinitionCol({ - {indexPattern && - reportDefinitions.map(({ field, custom, options }) => ( - - {!custom ? ( - - ) : ( - - )} - - ))} + {reportDefinitions.map(({ field, custom, options }) => ( + + {!custom ? ( + + ) : ( + + )} + + ))} {(hasOperationType || columnType === 'operation') && ( { - if (!custom && selectedReportDefinitions?.[fieldT] && fieldT !== field) { + if (!custom && indexPattern && selectedReportDefinitions?.[fieldT] && fieldT !== field) { const values = selectedReportDefinitions?.[fieldT]; const valueFilter = buildPhrasesFilter(fieldT, values, indexPattern)[0]; filtersN.push(valueFilter.query); @@ -64,16 +64,18 @@ export function ReportDefinitionField({ seriesId, field, dataSeries, onChange }: return ( - onChange(field, val)} - filters={queryFilters} - time={series.time} - fullWidth={true} - /> + {indexPattern && ( + onChange(field, val)} + filters={queryFilters} + time={series.time} + fullWidth={true} + /> + )} ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx index 7ca947fed0bc9..f35639388aac5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx @@ -15,7 +15,7 @@ describe('Series Builder ReportFilters', function () { const seriesId = 'test-series-id'; const dataViewSeries = getDefaultConfigs({ - reportType: 'dist', + reportType: 'data-distribution', indexPattern: mockIndexPattern, dataType: 'ux', }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx index f36d64ca5bbbd..f7cfe06c0d928 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx @@ -11,10 +11,9 @@ import { mockAppIndexPattern, render } from '../../rtl_helpers'; import { ReportTypesCol, SELECTED_DATA_TYPE_FOR_REPORT } from './report_types_col'; import { ReportTypes } from '../series_builder'; import { DEFAULT_TIME } from '../../configurations/constants'; -import { NEW_SERIES_KEY } from '../../hooks/use_series_storage'; describe('ReportTypesCol', function () { - const seriesId = 'test-series-id'; + const seriesId = 'performance-distribution'; mockAppIndexPattern(); @@ -40,7 +39,7 @@ describe('ReportTypesCol', function () { breakdown: 'user_agent.name', dataType: 'ux', reportDefinitions: {}, - reportType: 'kpi', + reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, }); expect(setSeries).toHaveBeenCalledTimes(1); @@ -49,11 +48,12 @@ describe('ReportTypesCol', function () { it('should set selected as filled', function () { const initSeries = { data: { - [NEW_SERIES_KEY]: { + [seriesId]: { dataType: 'synthetics' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, breakdown: 'monitor.status', time: { from: 'now-15m', to: 'now' }, + isNew: true, }, }, }; @@ -74,6 +74,7 @@ describe('ReportTypesCol', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'synthetics', time: DEFAULT_TIME, + isNew: true, }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx index 9fff8dae14a47..64c7b48c668b8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx @@ -7,27 +7,33 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { map } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import styled from 'styled-components'; -import { ReportViewTypeId, SeriesUrl } from '../../types'; +import { ReportViewType, SeriesUrl } from '../../types'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { DEFAULT_TIME } from '../../configurations/constants'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { ReportTypeItem, SELECT_DATA_TYPE } from '../series_builder'; interface Props { seriesId: string; - reportTypes: Array<{ id: ReportViewTypeId; label: string }>; + reportTypes: ReportTypeItem[]; } export function ReportTypesCol({ seriesId, reportTypes }: Props) { - const { setSeries, getSeries } = useSeriesStorage(); + const { setSeries, getSeries, firstSeries, firstSeriesId } = useSeriesStorage(); const { reportType: selectedReportType, ...restSeries } = getSeries(seriesId); - const { loading, hasData, selectedApp } = useAppIndexPatternContext(); + const { loading, hasData } = useAppIndexPatternContext(restSeries.dataType); - if (!loading && !hasData && selectedApp) { + if (!restSeries.dataType) { + return {SELECT_DATA_TYPE}; + } + + if (!loading && !hasData) { return ( firstSeriesId !== seriesId && reportType !== firstSeries.reportType + ), + 'reportType' + ); + return reportTypes?.length > 0 ? ( - {reportTypes.map(({ id: reportType, label }) => ( + {reportTypes.map(({ reportType, label }) => (