From 494d1decf5a5c595ea034a335e0116d9ba76330a Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Fri, 19 Feb 2021 11:54:28 +0200 Subject: [PATCH 01/32] [Indexpattern management] Use indexPatterns Service instead of savedObjects client (#91839) * [Index pattern management] Use indexPatterns Service instead of savedObjects client * Minor fixes * Keep the same test setup --- .../step_index_pattern.test.tsx | 4 +- .../step_index_pattern/step_index_pattern.tsx | 19 +---- .../edit_index_pattern/edit_index_pattern.tsx | 9 +-- .../index_pattern_table.tsx | 14 +--- .../public/components/utils.test.ts | 39 +++++----- .../public/components/utils.ts | 76 +++++++++---------- .../mount_management_section.tsx | 3 +- .../index_pattern_management/public/mocks.ts | 10 +-- .../index_pattern_management/public/types.ts | 2 - 9 files changed, 62 insertions(+), 114 deletions(-) diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx index ac025dba95bcd..8b4f751a4e3a3 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx @@ -7,7 +7,6 @@ */ import React from 'react'; -import { SavedObjectsFindResponsePublic } from 'kibana/public'; import { StepIndexPattern, canPreselectTimeField } from './step_index_pattern'; import { Header } from './components/header'; import { IndexPatternCreationConfig } from '../../../../../../../plugins/index_pattern_management/public'; @@ -43,8 +42,7 @@ const goToNextStep = () => {}; const mockContext = mockManagementPlugin.createIndexPatternManagmentContext(); -mockContext.savedObjects.client.find = async () => - Promise.resolve(({ savedObjects: [] } as unknown) as SavedObjectsFindResponsePublic); +mockContext.data.indexPatterns.getTitles = async () => Promise.resolve([]); mockContext.uiSettings.get.mockReturnValue(''); describe('StepIndexPattern', () => { diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx index d7038a754fc6b..052e454041181 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx @@ -10,11 +10,7 @@ import React, { Component } from 'react'; import { EuiSpacer, EuiCallOut, EuiSwitchEvent } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - indexPatterns, - IndexPatternAttributes, - UI_SETTINGS, -} from '../../../../../../../plugins/data/public'; +import { indexPatterns, UI_SETTINGS } from '../../../../../../../plugins/data/public'; import { getIndices, containsIllegalCharacters, @@ -118,18 +114,7 @@ export class StepIndexPattern extends Component { - const { - savedObjects, - } = await this.context.services.savedObjects.client.find({ - type: 'index-pattern', - fields: ['title'], - perPage: 10000, - }); - - const existingIndexPatterns = savedObjects.map((obj) => - obj && obj.attributes ? obj.attributes.title : '' - ) as string[]; - + const existingIndexPatterns = await this.context.services.data.indexPatterns.getTitles(); this.setState({ existingIndexPatterns }); }; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx index 30edc430f6b95..e314c00bc8176 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -26,8 +26,6 @@ import { useKibana } from '../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../types'; import { Tabs } from './tabs'; import { IndexHeader } from './index_header'; -import { IndexPatternTableItem } from '../types'; -import { getIndexPatterns } from '../utils'; export interface EditIndexPatternProps extends RouteComponentProps { indexPattern: IndexPattern; @@ -62,7 +60,6 @@ export const EditIndexPattern = withRouter( uiSettings, indexPatternManagementStart, overlays, - savedObjects, chrome, data, } = useKibana().services; @@ -97,11 +94,7 @@ export const EditIndexPattern = withRouter( const removePattern = () => { async function doRemove() { if (indexPattern.id === defaultIndex) { - const indexPatterns: IndexPatternTableItem[] = await getIndexPatterns( - savedObjects.client, - uiSettings.get('defaultIndex'), - indexPatternManagementStart - ); + const indexPatterns = await data.indexPatterns.getIdsWithTitle(); uiSettings.remove('defaultIndex'); const otherPatterns = filter(indexPatterns, (pattern) => { return pattern.id !== indexPattern.id; diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx index a2c30ea288445..b09246b5af8ad 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -69,13 +69,13 @@ interface Props extends RouteComponentProps { export const IndexPatternTable = ({ canSave, history }: Props) => { const { setBreadcrumbs, - savedObjects, uiSettings, indexPatternManagementStart, chrome, docLinks, application, http, + data, getMlCardState, } = useKibana().services; const [indexPatterns, setIndexPatterns] = useState([]); @@ -92,21 +92,15 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { history.push ); const gettedIndexPatterns: IndexPatternTableItem[] = await getIndexPatterns( - savedObjects.client, uiSettings.get('defaultIndex'), - indexPatternManagementStart + indexPatternManagementStart, + data.indexPatterns ); setIsLoadingIndexPatterns(false); setCreationOptions(options); setIndexPatterns(gettedIndexPatterns); })(); - }, [ - history.push, - indexPatterns.length, - indexPatternManagementStart, - uiSettings, - savedObjects.client, - ]); + }, [history.push, indexPatterns.length, indexPatternManagementStart, uiSettings, data]); const removeAliases = (item: MatchedItem) => !((item as unknown) as ResolveIndexResponseItemAlias).indices; diff --git a/src/plugins/index_pattern_management/public/components/utils.test.ts b/src/plugins/index_pattern_management/public/components/utils.test.ts index a1e60a4507b3c..15e0a65390f4d 100644 --- a/src/plugins/index_pattern_management/public/components/utils.test.ts +++ b/src/plugins/index_pattern_management/public/components/utils.test.ts @@ -5,36 +5,33 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import { IndexPatternsContract } from 'src/plugins/data/public'; import { getIndexPatterns } from './utils'; -import { coreMock } from '../../../../core/public/mocks'; import { mockManagementPlugin } from '../mocks'; -const { savedObjects } = coreMock.createStart(); -const mockManagementPluginStart = mockManagementPlugin.createStartContract(); - -(savedObjects.client.find as jest.Mock).mockResolvedValue({ - savedObjects: [ - { - id: 'test', - get: () => { - return 'test name'; +const indexPatternContractMock = ({ + getIdsWithTitle: jest.fn().mockReturnValue( + Promise.resolve([ + { + id: 'test', + title: 'test name', }, - }, - { - id: 'test1', - get: () => { - return 'test name 1'; + { + id: 'test1', + title: 'test name 1', }, - }, - ], -}); + ]) + ), + get: jest.fn().mockReturnValue(Promise.resolve({})), +} as unknown) as jest.Mocked; + +const mockManagementPluginStart = mockManagementPlugin.createStartContract(); test('getting index patterns', async () => { const indexPatterns = await getIndexPatterns( - savedObjects.client, 'test', - mockManagementPluginStart + mockManagementPluginStart, + indexPatternContractMock ); expect(indexPatterns).toMatchSnapshot(); }); diff --git a/src/plugins/index_pattern_management/public/components/utils.ts b/src/plugins/index_pattern_management/public/components/utils.ts index 59766e398e54e..5701a1e375204 100644 --- a/src/plugins/index_pattern_management/public/components/utils.ts +++ b/src/plugins/index_pattern_management/public/components/utils.ts @@ -6,54 +6,46 @@ * Side Public License, v 1. */ -import { IIndexPattern } from 'src/plugins/data/public'; -import { SavedObjectsClientContract } from 'src/core/public'; +import { IndexPatternsContract } from 'src/plugins/data/public'; import { IndexPatternManagementStart } from '../plugin'; export async function getIndexPatterns( - savedObjectsClient: SavedObjectsClientContract, defaultIndex: string, - indexPatternManagementStart: IndexPatternManagementStart + indexPatternManagementStart: IndexPatternManagementStart, + indexPatternsService: IndexPatternsContract ) { - return ( - savedObjectsClient - .find({ - type: 'index-pattern', - fields: ['title', 'type'], - perPage: 10000, - }) - .then((response) => - response.savedObjects - .map((pattern) => { - const id = pattern.id; - const title = pattern.get('title'); - const isDefault = defaultIndex === id; + const existingIndexPatterns = await indexPatternsService.getIdsWithTitle(); + const indexPatternsListItems = await Promise.all( + existingIndexPatterns.map(async ({ id, title }) => { + const isDefault = defaultIndex === id; + const pattern = await indexPatternsService.get(id); + const tags = (indexPatternManagementStart as IndexPatternManagementStart).list.getIndexPatternTags( + pattern, + isDefault + ); - const tags = (indexPatternManagementStart as IndexPatternManagementStart).list.getIndexPatternTags( - pattern, - isDefault - ); + return { + id, + title, + default: isDefault, + tags, + // the prepending of 0 at the default pattern takes care of prioritization + // so the sorting will but the default index on top + // or on bottom of a the table + sort: `${isDefault ? '0' : '1'}${title}`, + }; + }) + ); - return { - id, - title, - default: isDefault, - tags, - // the prepending of 0 at the default pattern takes care of prioritization - // so the sorting will but the default index on top - // or on bottom of a the table - sort: `${isDefault ? '0' : '1'}${title}`, - }; - }) - .sort((a, b) => { - if (a.sort < b.sort) { - return -1; - } else if (a.sort > b.sort) { - return 1; - } else { - return 0; - } - }) - ) || [] + return ( + indexPatternsListItems.sort((a, b) => { + if (a.sort < b.sort) { + return -1; + } else if (a.sort > b.sort) { + return 1; + } else { + return 0; + } + }) || [] ); } diff --git a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx index e47f60ad6fcdd..355f529fe0f75 100644 --- a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx @@ -41,7 +41,7 @@ export async function mountManagementSection( getMlCardState: () => MlCardState ) { const [ - { chrome, application, savedObjects, uiSettings, notifications, overlays, http, docLinks }, + { chrome, application, uiSettings, notifications, overlays, http, docLinks }, { data, indexPatternFieldEditor }, indexPatternManagementStart, ] = await getStartServices(); @@ -54,7 +54,6 @@ export async function mountManagementSection( const deps: IndexPatternManagmentContext = { chrome, application, - savedObjects, uiSettings, notifications, overlays, diff --git a/src/plugins/index_pattern_management/public/mocks.ts b/src/plugins/index_pattern_management/public/mocks.ts index 309d5a5611cd6..606f9edafbca9 100644 --- a/src/plugins/index_pattern_management/public/mocks.ts +++ b/src/plugins/index_pattern_management/public/mocks.ts @@ -75,14 +75,7 @@ const docLinks = { const createIndexPatternManagmentContext = (): { [key in keyof IndexPatternManagmentContext]: any; } => { - const { - chrome, - application, - savedObjects, - uiSettings, - notifications, - overlays, - } = coreMock.createStart(); + const { chrome, application, uiSettings, notifications, overlays } = coreMock.createStart(); const { http } = coreMock.createSetup(); const data = dataPluginMock.createStartContract(); const indexPatternFieldEditor = indexPatternFieldEditorPluginMock.createStartContract(); @@ -90,7 +83,6 @@ const createIndexPatternManagmentContext = (): { return { chrome, application, - savedObjects, uiSettings, notifications, overlays, diff --git a/src/plugins/index_pattern_management/public/types.ts b/src/plugins/index_pattern_management/public/types.ts index 62ee18ababc0b..58a138df633fd 100644 --- a/src/plugins/index_pattern_management/public/types.ts +++ b/src/plugins/index_pattern_management/public/types.ts @@ -11,7 +11,6 @@ import { ApplicationStart, IUiSettingsClient, OverlayStart, - SavedObjectsStart, NotificationsStart, DocLinksStart, HttpSetup, @@ -25,7 +24,6 @@ import { IndexPatternFieldEditorStart } from '../../index_pattern_field_editor/p export interface IndexPatternManagmentContext { chrome: ChromeStart; application: ApplicationStart; - savedObjects: SavedObjectsStart; uiSettings: IUiSettingsClient; notifications: NotificationsStart; overlays: OverlayStart; From bf7fdfc87cb04ecb5f6081d9056102d707997e9b Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 19 Feb 2021 11:30:40 +0100 Subject: [PATCH 02/32] [Lens] Pass used histogram interval to chart (#91370) --- ...ibana-plugin-plugins-data-public.search.md | 1 + .../search/aggs/buckets/histogram.test.ts | 22 +++++ .../common/search/aggs/buckets/histogram.ts | 42 ++++++-- .../get_number_histogram_interval.test.ts | 98 +++++++++++++++++++ .../utils/get_number_histogram_interval.ts | 28 ++++++ .../data/common/search/aggs/utils/index.ts | 1 + src/plugins/data/public/index.ts | 2 + src/plugins/data/public/public.api.md | 31 +++--- .../__snapshots__/expression.test.tsx.snap | 49 ++++++++++ .../xy_visualization/expression.test.tsx | 43 +++++++- .../public/xy_visualization/expression.tsx | 29 +++--- 11 files changed, 308 insertions(+), 38 deletions(-) create mode 100644 src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.test.ts create mode 100644 src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md index 4b3c915b49c2d..440fd25993d64 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md @@ -46,6 +46,7 @@ search: { boundLabel: string; intervalLabel: string; })[]; + getNumberHistogramIntervalByDatatableColumn: (column: import("../../expressions").DatatableColumn) => number | undefined; }; getRequestInspectorStats: typeof getRequestInspectorStats; getResponseInspectorStats: typeof getResponseInspectorStats; diff --git a/src/plugins/data/common/search/aggs/buckets/histogram.test.ts b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts index bddc7060af440..23693eaf5fca5 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts @@ -12,6 +12,7 @@ import { AggTypesDependencies } from '../agg_types'; import { BUCKET_TYPES } from './bucket_agg_types'; import { IBucketHistogramAggConfig, getHistogramBucketAgg, AutoBounds } from './histogram'; import { BucketAggType } from './bucket_agg_type'; +import { SerializableState } from 'src/plugins/expressions/common'; describe('Histogram Agg', () => { let aggTypesDependencies: AggTypesDependencies; @@ -230,6 +231,27 @@ describe('Histogram Agg', () => { expect(params.interval).toBeNaN(); }); + test('will serialize the auto interval along with the actually chosen interval and deserialize correctly', () => { + const aggConfigs = getAggConfigs({ + interval: 'auto', + field: { + name: 'field', + }, + }); + (aggConfigs.aggs[0] as IBucketHistogramAggConfig).setAutoBounds({ min: 0, max: 1000 }); + const serializedAgg = aggConfigs.aggs[0].serialize(); + const serializedIntervalParam = (serializedAgg.params as SerializableState).used_interval; + expect(serializedIntervalParam).toBe(500); + const freshHistogramAggConfig = getAggConfigs({ + interval: 100, + field: { + name: 'field', + }, + }).aggs[0]; + freshHistogramAggConfig.setParams(serializedAgg.params); + expect(freshHistogramAggConfig.getParam('interval')).toEqual('auto'); + }); + describe('interval scaling', () => { const getInterval = ( maxBars: number, diff --git a/src/plugins/data/common/search/aggs/buckets/histogram.ts b/src/plugins/data/common/search/aggs/buckets/histogram.ts index 5d6d7d509f08e..e04ebfe494ba9 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.ts @@ -8,6 +8,7 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { IUiSettingsClient } from 'kibana/public'; import { KBN_FIELD_TYPES, UI_SETTINGS } from '../../../../common'; import { AggTypesDependencies } from '../agg_types'; @@ -39,6 +40,7 @@ export interface IBucketHistogramAggConfig extends IBucketAggConfig { export interface AggParamsHistogram extends BaseAggParams { field: string; interval: number | string; + used_interval?: number | string; maxBars?: number; intervalBase?: number; min_doc_count?: boolean; @@ -141,17 +143,22 @@ export const getHistogramBucketAgg = ({ }); }, write(aggConfig, output) { - const values = aggConfig.getAutoBounds(); - - output.params.interval = calculateHistogramInterval({ - values, - interval: aggConfig.params.interval, - maxBucketsUiSettings: getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS), - maxBucketsUserInput: aggConfig.params.maxBars, - intervalBase: aggConfig.params.intervalBase, - esTypes: aggConfig.params.field?.spec?.esTypes || [], - }); + output.params.interval = calculateInterval(aggConfig, getConfig); + }, + }, + { + name: 'used_interval', + default: autoInterval, + shouldShow() { + return false; }, + write: () => {}, + serialize(val, aggConfig) { + if (!aggConfig) return undefined; + // store actually used auto interval in serialized agg config to be able to read it from the result data table meta information + return calculateInterval(aggConfig, getConfig); + }, + toExpressionAst: () => undefined, }, { name: 'maxBars', @@ -193,3 +200,18 @@ export const getHistogramBucketAgg = ({ }, ], }); + +function calculateInterval( + aggConfig: IBucketHistogramAggConfig, + getConfig: IUiSettingsClient['get'] +): any { + const values = aggConfig.getAutoBounds(); + return calculateHistogramInterval({ + values, + interval: aggConfig.params.interval, + maxBucketsUiSettings: getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS), + maxBucketsUserInput: aggConfig.params.maxBars, + intervalBase: aggConfig.params.intervalBase, + esTypes: aggConfig.params.field?.spec?.esTypes || [], + }); +} diff --git a/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.test.ts b/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.test.ts new file mode 100644 index 0000000000000..9b08426b551aa --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.test.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getNumberHistogramIntervalByDatatableColumn } from '.'; +import { BUCKET_TYPES } from '../buckets'; + +describe('getNumberHistogramIntervalByDatatableColumn', () => { + it('returns nothing on column from other data source', () => { + expect( + getNumberHistogramIntervalByDatatableColumn({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'essql', + }, + }) + ).toEqual(undefined); + }); + + it('returns nothing on non histogram column', () => { + expect( + getNumberHistogramIntervalByDatatableColumn({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: BUCKET_TYPES.TERMS, + }, + }, + }) + ).toEqual(undefined); + }); + + it('returns interval on resolved auto interval', () => { + expect( + getNumberHistogramIntervalByDatatableColumn({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: BUCKET_TYPES.HISTOGRAM, + params: { + interval: 'auto', + used_interval: 20, + }, + }, + }, + }) + ).toEqual(20); + }); + + it('returns interval on fixed interval', () => { + expect( + getNumberHistogramIntervalByDatatableColumn({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: BUCKET_TYPES.HISTOGRAM, + params: { + interval: 7, + used_interval: 7, + }, + }, + }, + }) + ).toEqual(7); + }); + + it('returns undefined if information is not available', () => { + expect( + getNumberHistogramIntervalByDatatableColumn({ + id: 'test', + name: 'test', + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: BUCKET_TYPES.HISTOGRAM, + params: {}, + }, + }, + }) + ).toEqual(undefined); + }); +}); diff --git a/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.ts b/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.ts new file mode 100644 index 0000000000000..e1c0cf2d69c60 --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/get_number_histogram_interval.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { DatatableColumn } from 'src/plugins/expressions/common'; +import type { AggParamsHistogram } from '../buckets'; +import { BUCKET_TYPES } from '../buckets/bucket_agg_types'; + +/** + * Helper function returning the used interval for data table column created by the histogramm agg type. + * "auto" will get expanded to the actually used interval. + * If the column is not a column created by a histogram aggregation of the esaggs data source, + * this function will return undefined. + */ +export const getNumberHistogramIntervalByDatatableColumn = (column: DatatableColumn) => { + if (column.meta.source !== 'esaggs') return; + if (column.meta.sourceParams?.type !== BUCKET_TYPES.HISTOGRAM) return; + const params = (column.meta.sourceParams.params as unknown) as AggParamsHistogram; + + if (!params.used_interval || typeof params.used_interval === 'string') { + return undefined; + } + return params.used_interval; +}; diff --git a/src/plugins/data/common/search/aggs/utils/index.ts b/src/plugins/data/common/search/aggs/utils/index.ts index 40451a0f66e0c..f90e8f88546f4 100644 --- a/src/plugins/data/common/search/aggs/utils/index.ts +++ b/src/plugins/data/common/search/aggs/utils/index.ts @@ -7,6 +7,7 @@ */ export * from './calculate_auto_time_expression'; +export { getNumberHistogramIntervalByDatatableColumn } from './get_number_histogram_interval'; export * from './date_interval_utils'; export * from './get_format_with_aggs'; export * from './ipv4_address'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index df799ede08a31..00bf0385487d8 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -308,6 +308,7 @@ import { parseInterval, toAbsoluteDates, boundsDescendingRaw, + getNumberHistogramIntervalByDatatableColumn, // expressions utils getRequestInspectorStats, getResponseInspectorStats, @@ -417,6 +418,7 @@ export const search = { termsAggFilter, toAbsoluteDates, boundsDescendingRaw, + getNumberHistogramIntervalByDatatableColumn, }, getRequestInspectorStats, getResponseInspectorStats, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 67423295dfe5e..36e34479ad2d1 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -2238,6 +2238,7 @@ export const search: { boundLabel: string; intervalLabel: string; })[]; + getNumberHistogramIntervalByDatatableColumn: (column: import("../../expressions").DatatableColumn) => number | undefined; }; getRequestInspectorStats: typeof getRequestInspectorStats; getResponseInspectorStats: typeof getResponseInspectorStats; @@ -2649,21 +2650,21 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:425:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:414:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/search/session/session_service.ts:42:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap index a2047b7bae669..9a32f1c331152 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap @@ -26,6 +26,13 @@ exports[`xy_expression XYChart component it renders area 1`] = ` "headerFormatter": [Function], } } + xDomain={ + Object { + "max": undefined, + "min": undefined, + "minInterval": 50, + } + } /> { }} /> ); - expect(component.find(Settings).prop('xDomain')).toBeUndefined(); + const xDomain = component.find(Settings).prop('xDomain'); + expect(xDomain).toEqual( + expect.objectContaining({ + min: undefined, + max: undefined, + }) + ); + }); + + test('it uses min interval if passed in', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + + ); + expect(component.find(Settings).prop('xDomain')).toEqual({ minInterval: 101 }); }); test('it renders bar', () => { @@ -1881,6 +1904,24 @@ describe('xy_expression', () => { expect(result).toEqual(5 * 60 * 1000); }); + it('should return interval of number histogram if available on first x axis columns', async () => { + xyProps.args.layers[0].xScaleType = 'linear'; + xyProps.data.tables.first.columns[2].meta = { + source: 'esaggs', + type: 'number', + field: 'someField', + sourceParams: { + type: 'histogram', + params: { + interval: 'auto', + used_interval: 5, + }, + }, + }; + const result = await calculateMinInterval(xyProps, jest.fn().mockResolvedValue(undefined)); + expect(result).toEqual(5); + }); + it('should return undefined if data table is empty', async () => { xyProps.data.tables.first.rows = []; const result = await calculateMinInterval( diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 7f6414b40cb90..eda08715b394e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -199,13 +199,20 @@ export async function calculateMinInterval( const filteredLayers = getFilteredLayers(layers, data); if (filteredLayers.length === 0) return; const isTimeViz = data.dateRange && filteredLayers.every((l) => l.xScaleType === 'time'); - - if (!isTimeViz) return; - const dateColumn = data.tables[filteredLayers[0].layerId].columns.find( + const xColumn = data.tables[filteredLayers[0].layerId].columns.find( (column) => column.id === filteredLayers[0].xAccessor ); - if (!dateColumn) return; - const dateMetaData = await getIntervalByColumn(dateColumn); + + if (!xColumn) return; + if (!isTimeViz) { + const histogramInterval = search.aggs.getNumberHistogramIntervalByDatatableColumn(xColumn); + if (typeof histogramInterval === 'number') { + return histogramInterval; + } else { + return undefined; + } + } + const dateMetaData = await getIntervalByColumn(xColumn); if (!dateMetaData) return; const intervalDuration = search.aggs.parseInterval(dateMetaData.interval); if (!intervalDuration) return; @@ -381,13 +388,11 @@ export function XYChart({ const isTimeViz = data.dateRange && filteredLayers.every((l) => l.xScaleType === 'time'); const isHistogramViz = filteredLayers.every((l) => l.isHistogram); - const xDomain = isTimeViz - ? { - min: data.dateRange?.fromDate.getTime(), - max: data.dateRange?.toDate.getTime(), - minInterval, - } - : undefined; + const xDomain = { + min: isTimeViz ? data.dateRange?.fromDate.getTime() : undefined, + max: isTimeViz ? data.dateRange?.toDate.getTime() : undefined, + minInterval, + }; const getYAxesTitles = ( axisSeries: Array<{ layer: string; accessor: string }>, From 5bfcc096a6b6f24fd42eea321636181efbb4fe64 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Fri, 19 Feb 2021 07:40:16 -0500 Subject: [PATCH 03/32] [Fleet] Don't error on missing package_assets value (#91744) ## Summary closes https://github.com/elastic/kibana/issues/89111 * Update TS type to make `package_assets` key in EPM packages saved object optional * Update two places in code to deal with optional vs required property ### Checklist - [ ] [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 #### Manual testing 1. checkout `7.10` branch 1. **start ES:** `nvm use; yarn kbn bootstrap; yarn es snapshot --version 7.10.3 --license=trial -E xpack.security.authc.api_key.enabled=true -E path.data=../data` 1. **start Kibana**: `yarn start --no-base-path` 1. **run** `curl -X POST -H 'kbn-xsrf: 1234' --user elastic:changeme localhost:5601/api/fleet/setup` 2. **observe** `{"is_initialized: true}` 1. checkout `7.11` branch 1. **start ES:** `nvm use; yarn kbn bootstrap; yarn es snapshot --version 7.11.1 --license=trial -E xpack.security.authc.api_key.enabled=true -E path.data=../data` 1. **start Kibana**: `yarn start --no-base-path` 1. **run** `curl -X POST -H 'kbn-xsrf: 1234' --user elastic:changeme localhost:5601/api/fleet/setup` 1. **observe** `{"is_initialized: true}` 1. checkout `master` branch 1. **start ES:** `nvm use; yarn kbn bootstrap; yarn es snapshot --version 8.0.0 --license=trial -E xpack.security.authc.api_key.enabled=true -E path.data=../data` 1. **start Kibana**: `yarn start --no-base-path` 1. **run** `curl -X POST -H 'kbn-xsrf: 1234' --user elastic:changeme localhost:5601/api/fleet/setup` 1. **observe error** {"statusCode":500,"error":"Internal Server Error","message":"Cannot read property 'map' of undefined"} 1. checkout this PR `8911-fleet-startup-error` 1. **start ES:** `nvm use; yarn kbn bootstrap; yarn es snapshot --version 8.0.0 --license=trial -E xpack.security.authc.api_key.enabled=true -E path.data=../data` 1. **start Kibana**: `yarn start --no-base-path` 1. **run** `curl -X POST -H 'kbn-xsrf: 1234' --user elastic:changeme localhost:5601/api/fleet/setup` 1. **observe success** `{"is_initialized: true}` **_Notes_** * _you might need to do a `yarn kbn clean` when starting kibana if it fails. There have been some big changes in the tooling recently_ --- x-pack/plugins/fleet/common/types/models/epm.ts | 2 +- .../plugins/fleet/server/services/epm/archive/storage.ts | 3 ++- x-pack/plugins/fleet/server/services/epm/packages/get.ts | 8 ++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index e7e5a931b7429..5c99831eaac34 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -273,7 +273,7 @@ export type PackageInfo = export interface Installation extends SavedObjectAttributes { installed_kibana: KibanaAssetReference[]; installed_es: EsAssetReference[]; - package_assets: PackageAssetReference[]; + package_assets?: PackageAssetReference[]; es_index_patterns: Record; name: string; version: string; diff --git a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts index 4144146896628..20e1e8825fbd8 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts @@ -83,9 +83,10 @@ export async function archiveEntryToESDocument(opts: { export async function removeArchiveEntries(opts: { savedObjectsClient: SavedObjectsClientContract; - refs: PackageAssetReference[]; + refs?: PackageAssetReference[]; }) { const { savedObjectsClient, refs } = opts; + if (!refs) return; const results = await Promise.all( refs.map((ref) => savedObjectsClient.delete(ASSETS_SAVED_OBJECT_TYPE, ref.id)) ); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index 0fac68426b73e..c07b88a45e6dc 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -16,6 +16,7 @@ import { import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { ArchivePackage, RegistryPackage, EpmPackageAdditions } from '../../../../common/types'; import { Installation, PackageInfo, KibanaAssetType } from '../../../types'; +import { IngestManagerError } from '../../../errors'; import * as Registry from '../registry'; import { createInstallableFrom, isRequiredPackage } from './index'; import { getEsPackage } from '../archive/storage'; @@ -185,7 +186,8 @@ export async function getPackageFromSource(options: { name: pkgName, version: pkgVersion, }); - if (!res) { + + if (!res && installedPkg.package_assets) { res = await getEsPackage( pkgName, pkgVersion, @@ -207,7 +209,9 @@ export async function getPackageFromSource(options: { // else package is not installed or installed and missing from cache and storage and installed from registry res = await Registry.getRegistryPackage(pkgName, pkgVersion); } - if (!res) throw new Error(`package info for ${pkgName}-${pkgVersion} does not exist`); + if (!res) { + throw new IngestManagerError(`package info for ${pkgName}-${pkgVersion} does not exist`); + } return { paths: res.paths, packageInfo: res.packageInfo, From f8fd08fbcd3b75f7b34c13eb1b954523fe2a2abe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Fri, 19 Feb 2021 14:34:52 +0100 Subject: [PATCH 04/32] Refactored component edit policy tests into separate folders and using client integration testing setup (#91657) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__snapshots__/policy_table.test.tsx.snap | 0 .../edit_policy/constants.ts | 31 + .../edit_policy/edit_policy.helpers.tsx | 183 ++-- .../edit_policy/edit_policy.test.ts | 22 +- .../cold_phase_validation.test.ts | 125 +++ .../delete_phase_validation.ts | 81 ++ .../hot_phase_validation.test.ts | 174 ++++ .../policy_name_validation.test.ts | 100 ++ .../warm_phase_validation.test.ts | 171 ++++ .../reactive_form/node_allocation.test.ts | 382 +++++++ .../reactive_form/reactive_form.test.ts | 143 +++ .../helpers/http_requests.ts | 15 +- .../__jest__/components/README.md | 8 - .../__jest__/components/edit_policy.test.tsx | 967 ------------------ .../components/helpers/edit_policy.ts | 31 - .../components/helpers/http_requests.ts | 60 -- .../__jest__/components/helpers/index.ts | 12 - .../{components => }/policy_table.test.tsx | 14 +- .../common/types/api.ts | 10 + 19 files changed, 1370 insertions(+), 1159 deletions(-) rename x-pack/plugins/index_lifecycle_management/__jest__/{components => }/__snapshots__/policy_table.test.tsx.snap (100%) create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/components/README.md delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts rename x-pack/plugins/index_lifecycle_management/__jest__/{components => }/policy_table.test.tsx (92%) diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap similarity index 100% rename from x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap rename to x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts index a63203656dc46..2c8fbfc749a82 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +import moment from 'moment-timezone'; + import { PolicyFromES } from '../../../common/types'; export const POLICY_NAME = 'my_policy'; @@ -234,3 +236,32 @@ export const POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS = ({ }, name: POLICY_NAME, } as any) as PolicyFromES; + +export const getGeneratedPolicies = (): PolicyFromES[] => { + const policy = { + phases: { + hot: { + min_age: '0s', + actions: { + rollover: { + max_size: '1gb', + }, + }, + }, + }, + }; + const policies: PolicyFromES[] = []; + for (let i = 0; i < 105; i++) { + policies.push({ + version: i, + modified_date: moment().subtract(i, 'days').toISOString(), + linkedIndices: i % 2 === 0 ? [`index${i}`] : undefined, + name: `testy${i}`, + policy: { + ...policy, + name: `testy${i}`, + }, + }); + } + return policies; +}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index 83a13f0523a40..a9845c2315604 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -21,10 +21,12 @@ import { KibanaContextProvider } from '../../../public/shared_imports'; import { AppServicesContext } from '../../../public/types'; import { createBreadcrumbsMock } from '../../../public/application/services/breadcrumbs.mock'; +import { TestSubjects } from '../helpers'; +import { POLICY_NAME } from './constants'; + type Phases = keyof PolicyPhases; -import { POLICY_NAME } from './constants'; -import { TestSubjects } from '../helpers'; +window.scrollTo = jest.fn(); jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); @@ -46,14 +48,17 @@ jest.mock('@elastic/eui', () => { }; }); -const testBedConfig: TestBedConfig = { - memoryRouter: { - initialEntries: [`/policies/edit/${POLICY_NAME}`], - componentRoutePath: `/policies/edit/:policyName`, - }, - defaultProps: { - getUrlForApp: () => {}, - }, +const getTestBedConfig = (testBedConfigArgs?: Partial): TestBedConfig => { + return { + memoryRouter: { + initialEntries: [`/policies/edit/${POLICY_NAME}`], + componentRoutePath: `/policies/edit/:policyName`, + }, + defaultProps: { + getUrlForApp: () => {}, + }, + ...testBedConfigArgs, + }; }; const breadcrumbService = createBreadcrumbsMock(); @@ -72,13 +77,22 @@ const MyComponent = ({ appServicesContext, ...rest }: any) => { ); }; -const initTestBed = registerTestBed(MyComponent, testBedConfig); +const initTestBed = (arg?: { + appServicesContext?: Partial; + testBedConfig?: Partial; +}) => { + const { testBedConfig: testBedConfigArgs, ...rest } = arg || {}; + return registerTestBed(MyComponent, getTestBedConfig(testBedConfigArgs))(rest); +}; type SetupReturn = ReturnType; export type EditPolicyTestBed = SetupReturn extends Promise ? U : SetupReturn; -export const setup = async (arg?: { appServicesContext: Partial }) => { +export const setup = async (arg?: { + appServicesContext?: Partial; + testBedConfig?: Partial; +}) => { const testBed = await initTestBed(arg); const { find, component, form, exists } = testBed; @@ -169,34 +183,15 @@ export const setup = async (arg?: { appServicesContext: Partial createFormToggleAction(`enablePhaseSwitch-${phase}`); - const setMinAgeValue = (phase: Phases) => createFormSetValueAction(`${phase}-selectedMinimumAge`); - - const setMinAgeUnits = (phase: Phases) => - createFormSetValueAction(`${phase}-selectedMinimumAgeUnits`); - - const setDataAllocation = (phase: Phases) => async (value: DataTierAllocationType) => { - act(() => { - find(`${phase}-dataTierAllocationControls.dataTierSelect`).simulate('click'); - }); - component.update(); - await act(async () => { - switch (value) { - case 'node_roles': - find(`${phase}-dataTierAllocationControls.defaultDataAllocationOption`).simulate('click'); - break; - case 'node_attrs': - find(`${phase}-dataTierAllocationControls.customDataAllocationOption`).simulate('click'); - break; - default: - find(`${phase}-dataTierAllocationControls.noneDataAllocationOption`).simulate('click'); - } - }); - component.update(); + const createMinAgeActions = (phase: Phases) => { + return { + hasMinAgeInput: () => exists(`${phase}-selectedMinimumAge`), + setMinAgeValue: createFormSetValueAction(`${phase}-selectedMinimumAge`), + setMinAgeUnits: createFormSetValueAction(`${phase}-selectedMinimumAgeUnits`), + hasRolloverTipOnMinAge: () => exists(`${phase}-rolloverMinAgeInputIconTip`), + }; }; - const setSelectedNodeAttribute = (phase: Phases) => - createFormSetValueAction(`${phase}-selectedNodeAttrs`); - const setReplicas = (phase: Phases) => async (value: string) => { if (!exists(`${phase}-selectedReplicaCount`)) { await createFormToggleAction(`${phase}-setReplicasSwitch`)(true); @@ -216,8 +211,12 @@ export const setup = async (arg?: { appServicesContext: Partial exists('freezeSwitch'); - const setReadonly = (phase: Phases) => async (value: boolean) => { - await createFormToggleAction(`${phase}-readonlySwitch`)(value); + const createReadonlyActions = (phase: Phases) => { + const toggleSelector = `${phase}-readonlySwitch`; + return { + readonlyExists: () => exists(toggleSelector), + toggleReadonly: createFormToggleAction(toggleSelector), + }; }; const createSearchableSnapshotActions = (phase: Phases) => { @@ -271,17 +270,93 @@ export const setup = async (arg?: { appServicesContext: Partial (): boolean => - exists(`${phase}-rolloverMinAgeInputIconTip`); + const hasRolloverSettingRequiredCallout = (): boolean => exists('rolloverSettingsRequired'); + + const createNodeAllocationActions = (phase: Phases) => { + const controlsSelector = `${phase}-dataTierAllocationControls`; + const dataTierSelector = `${controlsSelector}.dataTierSelect`; + const nodeAttrsSelector = `${phase}-selectedNodeAttrs`; + + return { + hasDataTierAllocationControls: () => exists(controlsSelector), + openNodeAttributesSection: async () => { + await act(async () => { + find(dataTierSelector).simulate('click'); + }); + component.update(); + }, + hasNodeAttributesSelect: (): boolean => exists(nodeAttrsSelector), + getNodeAttributesSelectOptions: () => find(nodeAttrsSelector).find('option'), + setDataAllocation: async (value: DataTierAllocationType) => { + act(() => { + find(dataTierSelector).simulate('click'); + }); + component.update(); + await act(async () => { + switch (value) { + case 'node_roles': + find(`${controlsSelector}.defaultDataAllocationOption`).simulate('click'); + break; + case 'node_attrs': + find(`${controlsSelector}.customDataAllocationOption`).simulate('click'); + break; + default: + find(`${controlsSelector}.noneDataAllocationOption`).simulate('click'); + } + }); + component.update(); + }, + setSelectedNodeAttribute: createFormSetValueAction(nodeAttrsSelector), + hasNoNodeAttrsWarning: () => exists('noNodeAttributesWarning'), + hasDefaultAllocationWarning: () => exists('defaultAllocationWarning'), + hasDefaultAllocationNotice: () => exists('defaultAllocationNotice'), + hasNodeDetailsFlyout: () => exists(`${phase}-viewNodeDetailsFlyoutButton`), + openNodeDetailsFlyout: async () => { + await act(async () => { + find(`${phase}-viewNodeDetailsFlyoutButton`).simulate('click'); + }); + component.update(); + }, + }; + }; + + const expectErrorMessages = (expectedMessages: string[]) => { + const errorMessages = component.find('.euiFormErrorText'); + expect(errorMessages.length).toBe(expectedMessages.length); + expectedMessages.forEach((expectedErrorMessage) => { + let foundErrorMessage; + for (let i = 0; i < errorMessages.length; i++) { + if (errorMessages.at(i).text() === expectedErrorMessage) { + foundErrorMessage = true; + } + } + expect(foundErrorMessage).toBe(true); + }); + }; + + /* + * For new we rely on a setTimeout to ensure that error messages have time to populate + * the form object before we look at the form object. See: + * x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx + * for where this logic lives. + */ + const runTimers = () => { + act(() => { + jest.runAllTimers(); + }); + component.update(); + }; return { ...testBed, + runTimers, actions: { saveAsNewPolicy: createFormToggleAction('saveAsNewSwitch'), setPolicyName: createFormSetValueAction('policyNameField'), setWaitForSnapshotPolicy, savePolicy, hasGlobalErrorCallout: () => exists('policyFormErrorsCallout'), + expectErrorMessages, timeline: { hasHotPhase: () => exists('ilmTimelineHotPhase'), hasWarmPhase: () => exists('ilmTimelineWarmPhase'), @@ -294,46 +369,40 @@ export const setup = async (arg?: { appServicesContext: Partial exists('phaseErrorIndicator-hot'), ...createForceMergeActions('hot'), ...createIndexPriorityActions('hot'), ...createShrinkActions('hot'), - setReadonly: setReadonly('hot'), + ...createReadonlyActions('hot'), ...createSearchableSnapshotActions('hot'), }, warm: { enable: enable('warm'), - setMinAgeValue: setMinAgeValue('warm'), - setMinAgeUnits: setMinAgeUnits('warm'), - setDataAllocation: setDataAllocation('warm'), - setSelectedNodeAttribute: setSelectedNodeAttribute('warm'), + ...createMinAgeActions('warm'), setReplicas: setReplicas('warm'), hasErrorIndicator: () => exists('phaseErrorIndicator-warm'), - hasRolloverTipOnMinAge: hasRolloverTipOnMinAge('warm'), ...createShrinkActions('warm'), ...createForceMergeActions('warm'), - setReadonly: setReadonly('warm'), + ...createReadonlyActions('warm'), ...createIndexPriorityActions('warm'), + ...createNodeAllocationActions('warm'), }, cold: { enable: enable('cold'), - setMinAgeValue: setMinAgeValue('cold'), - setMinAgeUnits: setMinAgeUnits('cold'), - setDataAllocation: setDataAllocation('cold'), - setSelectedNodeAttribute: setSelectedNodeAttribute('cold'), + ...createMinAgeActions('cold'), setReplicas: setReplicas('cold'), setFreeze, freezeExists, hasErrorIndicator: () => exists('phaseErrorIndicator-cold'), - hasRolloverTipOnMinAge: hasRolloverTipOnMinAge('cold'), ...createIndexPriorityActions('cold'), ...createSearchableSnapshotActions('cold'), + ...createNodeAllocationActions('cold'), }, delete: { + isShown: () => exists('delete-phaseContent'), ...createToggleDeletePhaseActions(), - hasRolloverTipOnMinAge: hasRolloverTipOnMinAge('delete'), - setMinAgeValue: setMinAgeValue('delete'), - setMinAgeUnits: setMinAgeUnits('delete'), + ...createMinAgeActions('delete'), }, }, }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index 859b4adce5028..7fe5c6f50d046 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -25,8 +25,6 @@ import { getDefaultHotPhasePolicy, } from './constants'; -window.scrollTo = jest.fn(); - describe('', () => { let testBed: EditPolicyTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); @@ -127,7 +125,7 @@ describe('', () => { await actions.hot.setBestCompression(true); await actions.hot.toggleShrink(true); await actions.hot.setShrink('2'); - await actions.hot.setReadonly(true); + await actions.hot.toggleReadonly(true); await actions.hot.toggleIndexPriority(true); await actions.hot.setIndexPriority('123'); @@ -271,7 +269,7 @@ describe('', () => { await actions.warm.toggleForceMerge(true); await actions.warm.setForcemergeSegmentsCount('123'); await actions.warm.setBestCompression(true); - await actions.warm.setReadonly(true); + await actions.warm.toggleReadonly(true); await actions.warm.setIndexPriority('123'); await actions.savePolicy(); const latestRequest = server.requests[server.requests.length - 1]; @@ -918,6 +916,7 @@ describe('', () => { }); describe('policy error notifications', () => { + let runTimers: () => void; beforeAll(() => { jest.useFakeTimers(); }); @@ -925,6 +924,7 @@ describe('', () => { afterAll(() => { jest.useRealTimers(); }); + beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); httpRequestsMockHelpers.setListNodes({ @@ -940,19 +940,9 @@ describe('', () => { const { component } = testBed; component.update(); - }); - // For new we rely on a setTimeout to ensure that error messages have time to populate - // the form object before we look at the form object. See: - // x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx - // for where this logic lives. - const runTimers = () => { - const { component } = testBed; - act(() => { - jest.runAllTimers(); - }); - component.update(); - }; + ({ runTimers } = testBed); + }); test('shows phase error indicators correctly', async () => { // This test simulates a user configuring a policy phase by phase. The flow is the following: diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts new file mode 100644 index 0000000000000..c5c4bb1be87e0 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts @@ -0,0 +1,125 @@ +/* + * Copyright 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 { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' cold phase validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('mypolicy'); + await actions.cold.enable(true); + + ({ runTimers } = testBed); + }); + + describe('timing', () => { + test(`doesn't allow empty timing`, async () => { + const { actions } = testBed; + + await actions.cold.setMinAgeValue(''); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for phase timing`, async () => { + const { actions } = testBed; + + await actions.cold.setMinAgeValue('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + + test(`doesn't allow -1 for timing`, async () => { + const { actions } = testBed; + + await actions.cold.setMinAgeValue('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + }); + + describe('replicas', () => { + test(`doesn't allow -1 for replicas`, async () => { + const { actions } = testBed; + + await actions.cold.setReplicas('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for replicas`, async () => { + const { actions } = testBed; + + await actions.cold.setReplicas('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + }); + + describe('index priority', () => { + test(`doesn't allow -1 for index priority`, async () => { + const { actions } = testBed; + + await actions.cold.setIndexPriority('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for index priority`, async () => { + const { actions } = testBed; + + await actions.cold.setIndexPriority('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.ts new file mode 100644 index 0000000000000..a13aaa02dcd06 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.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 { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' delete phase validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('mypolicy'); + await actions.delete.enablePhase(); + + ({ runTimers } = testBed); + }); + + describe('timing', () => { + test(`doesn't allow empty timing`, async () => { + const { actions } = testBed; + + await actions.delete.setMinAgeValue(''); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for phase timing`, async () => { + const { actions } = testBed; + + await actions.delete.setMinAgeValue('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + + test(`doesn't allow -1 for timing`, async () => { + const { actions } = testBed; + + await actions.delete.setMinAgeValue('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts new file mode 100644 index 0000000000000..7c1d687b27e3d --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts @@ -0,0 +1,174 @@ +/* + * Copyright 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 { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' hot phase validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('mypolicy'); + + ({ runTimers } = testBed); + }); + + describe('rollover', () => { + test(`doesn't allow no max size, no max age and no max docs`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + expect(actions.hot.hasRolloverSettingRequiredCallout()).toBeFalsy(); + + await actions.hot.setMaxSize(''); + await actions.hot.setMaxAge(''); + await actions.hot.setMaxDocs(''); + + runTimers(); + + expect(actions.hot.hasRolloverSettingRequiredCallout()).toBeTruthy(); + }); + + test(`doesn't allow -1 for max size`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxSize('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + + test(`doesn't allow 0 for max size`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxSize('0'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + + test(`doesn't allow -1 for max age`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxAge('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + + test(`doesn't allow 0 for max age`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxAge('0'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + + test(`doesn't allow -1 for max docs`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxDocs('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + + test(`doesn't allow 0 for max docs`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxDocs('0'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + }); + + describe('forcemerge', () => { + test(`doesn't allow 0 for forcemerge`, async () => { + const { actions } = testBed; + await actions.hot.toggleForceMerge(true); + await actions.hot.setForcemergeSegmentsCount('0'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + test(`doesn't allow -1 for forcemerge`, async () => { + const { actions } = testBed; + await actions.hot.toggleForceMerge(true); + await actions.hot.setForcemergeSegmentsCount('-1'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + }); + + describe('shrink', () => { + test(`doesn't allow 0 for shrink`, async () => { + const { actions } = testBed; + await actions.hot.toggleShrink(true); + await actions.hot.setShrink('0'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + test(`doesn't allow -1 for shrink`, async () => { + const { actions } = testBed; + await actions.hot.toggleShrink(true); + await actions.hot.setShrink('-1'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + }); + + describe('index priority', () => { + test(`doesn't allow -1 for index priority`, async () => { + const { actions } = testBed; + + await actions.hot.setIndexPriority('-1'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for index priority`, async () => { + const { actions } = testBed; + + await actions.hot.setIndexPriority('0'); + runTimers(); + actions.expectErrorMessages([]); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts new file mode 100644 index 0000000000000..0acb425b1d975 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.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 { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { getGeneratedPolicies } from '../constants'; + +describe(' policy name validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies(getGeneratedPolicies()); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + + ({ runTimers } = testBed); + }); + + test(`doesn't allow empty policy name`, async () => { + const { actions } = testBed; + await actions.savePolicy(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.policyNameRequiredMessage]); + }); + + test(`doesn't allow policy name with space`, async () => { + const { actions } = testBed; + await actions.setPolicyName('my policy'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.policyNameContainsInvalidChars]); + }); + + test(`doesn't allow policy name that is already used`, async () => { + const { actions } = testBed; + await actions.setPolicyName('testy0'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.policyNameAlreadyUsedErrorMessage]); + }); + + test(`doesn't allow to save as new policy but using the same name`, async () => { + await act(async () => { + testBed = await setup({ + testBedConfig: { + memoryRouter: { + initialEntries: [`/policies/edit/testy0`], + componentRoutePath: `/policies/edit/:policyName`, + }, + }, + }); + }); + const { component, actions } = testBed; + component.update(); + + ({ runTimers } = testBed); + + await actions.saveAsNewPolicy(true); + runTimers(); + await actions.savePolicy(); + actions.expectErrorMessages([ + i18nTexts.editPolicy.errors.policyNameMustBeDifferentErrorMessage, + ]); + }); + + test(`doesn't allow policy name with comma`, async () => { + const { actions } = testBed; + await actions.setPolicyName('my,policy'); + runTimers(); + actions.expectErrorMessages([i18nTexts.editPolicy.errors.policyNameContainsInvalidChars]); + }); + + test(`doesn't allow policy name starting with underscore`, async () => { + const { actions } = testBed; + await actions.setPolicyName('_mypolicy'); + runTimers(); + actions.expectErrorMessages([ + i18nTexts.editPolicy.errors.policyNameStartsWithUnderscoreErrorMessage, + ]); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts new file mode 100644 index 0000000000000..2121dba8e06f6 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts @@ -0,0 +1,171 @@ +/* + * Copyright 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 { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' warm phase validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('mypolicy'); + await actions.warm.enable(true); + + ({ runTimers } = testBed); + }); + + describe('timing', () => { + test(`doesn't allow empty timing`, async () => { + const { actions } = testBed; + + await actions.warm.setMinAgeValue(''); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for phase timing`, async () => { + const { actions } = testBed; + + await actions.warm.setMinAgeValue('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + + test(`doesn't allow -1 for timing`, async () => { + const { actions } = testBed; + + await actions.warm.setMinAgeValue('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + }); + + describe('replicas', () => { + test(`doesn't allow -1 for replicas`, async () => { + const { actions } = testBed; + + await actions.warm.setReplicas('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for replicas`, async () => { + const { actions } = testBed; + + await actions.warm.setReplicas('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + }); + + describe('shrink', () => { + test(`doesn't allow 0 for shrink`, async () => { + const { actions } = testBed; + + await actions.warm.toggleShrink(true); + await actions.warm.setShrink('0'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + test(`doesn't allow -1 for shrink`, async () => { + const { actions } = testBed; + + await actions.warm.toggleShrink(true); + await actions.warm.setShrink('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + }); + + describe('forcemerge', () => { + test(`doesn't allow 0 for forcemerge`, async () => { + const { actions } = testBed; + + await actions.warm.toggleForceMerge(true); + await actions.warm.setForcemergeSegmentsCount('0'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + test(`doesn't allow -1 for forcemerge`, async () => { + const { actions } = testBed; + + await actions.warm.toggleForceMerge(true); + await actions.warm.setForcemergeSegmentsCount('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); + }); + }); + + describe('index priority', () => { + test(`doesn't allow -1 for index priority`, async () => { + const { actions } = testBed; + + await actions.warm.setIndexPriority('-1'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); + }); + + test(`allows 0 for index priority`, async () => { + const { actions } = testBed; + + await actions.warm.setIndexPriority('0'); + + runTimers(); + + actions.expectErrorMessages([]); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts new file mode 100644 index 0000000000000..113698fdf6df2 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts @@ -0,0 +1,382 @@ +/* + * Copyright 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 { act } from 'react-dom/test-utils'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' node allocation', () => { + let testBed: EditPolicyTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + server.respondImmediately = true; + httpRequestsMockHelpers.setLoadPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + describe('warm phase', () => { + test('shows spinner for node attributes input when loading', async () => { + server.respondImmediately = false; + + const { actions, component } = testBed; + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeTruthy(); + expect(actions.warm.hasDataTierAllocationControls()).toBeTruthy(); + + expect(component.find('.euiCallOut--warning').exists()).toBeFalsy(); + expect(actions.warm.hasNodeAttributesSelect()).toBeFalsy(); + }); + + test('shows warning instead of node attributes input when none exist', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + await actions.warm.setDataAllocation('node_attrs'); + expect(actions.warm.hasNoNodeAttrsWarning()).toBeTruthy(); + expect(actions.warm.hasNodeAttributesSelect()).toBeFalsy(); + }); + + test('shows node attributes input when attributes exist', async () => { + const { actions, component } = testBed; + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + await actions.warm.setDataAllocation('node_attrs'); + expect(actions.warm.hasNoNodeAttrsWarning()).toBeFalsy(); + expect(actions.warm.hasNodeAttributesSelect()).toBeTruthy(); + expect(actions.warm.getNodeAttributesSelectOptions().length).toBe(2); + }); + + test('shows view node attributes link when attribute selected and shows flyout when clicked', async () => { + const { actions, component } = testBed; + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + await actions.warm.setDataAllocation('node_attrs'); + expect(actions.warm.hasNoNodeAttrsWarning()).toBeFalsy(); + expect(actions.warm.hasNodeAttributesSelect()).toBeTruthy(); + + expect(actions.warm.hasNodeDetailsFlyout()).toBeFalsy(); + expect(actions.warm.getNodeAttributesSelectOptions().length).toBe(2); + await actions.warm.setSelectedNodeAttribute('attribute:true'); + + await actions.warm.openNodeDetailsFlyout(); + expect(actions.warm.hasNodeDetailsFlyout()).toBeTruthy(); + }); + + test('shows default allocation warning when no node roles are found', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: {}, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.warm.hasDefaultAllocationWarning()).toBeTruthy(); + }); + + test('shows default allocation notice when hot tier exists, but not warm tier', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data_hot: ['test'], data_cold: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.warm.hasDefaultAllocationNotice()).toBeTruthy(); + }); + + test(`doesn't show default allocation notice when node with "data" role exists`, async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.warm.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.warm.hasDefaultAllocationNotice()).toBeFalsy(); + }); + }); + + describe('cold phase', () => { + test('shows spinner for node attributes input when loading', async () => { + server.respondImmediately = false; + + const { actions, component } = testBed; + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeTruthy(); + expect(actions.cold.hasDataTierAllocationControls()).toBeTruthy(); + + expect(component.find('.euiCallOut--warning').exists()).toBeFalsy(); + expect(actions.cold.hasNodeAttributesSelect()).toBeFalsy(); + }); + + test('shows warning instead of node attributes input when none exist', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + await actions.cold.setDataAllocation('node_attrs'); + expect(actions.cold.hasNoNodeAttrsWarning()).toBeTruthy(); + expect(actions.cold.hasNodeAttributesSelect()).toBeFalsy(); + }); + + test('shows node attributes input when attributes exist', async () => { + const { actions, component } = testBed; + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + await actions.cold.setDataAllocation('node_attrs'); + expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy(); + expect(actions.cold.hasNodeAttributesSelect()).toBeTruthy(); + expect(actions.cold.getNodeAttributesSelectOptions().length).toBe(2); + }); + + test('shows view node attributes link when attribute selected and shows flyout when clicked', async () => { + const { actions, component } = testBed; + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + await actions.cold.setDataAllocation('node_attrs'); + expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy(); + expect(actions.cold.hasNodeAttributesSelect()).toBeTruthy(); + + expect(actions.cold.hasNodeDetailsFlyout()).toBeFalsy(); + expect(actions.cold.getNodeAttributesSelectOptions().length).toBe(2); + await actions.cold.setSelectedNodeAttribute('attribute:true'); + + await actions.cold.openNodeDetailsFlyout(); + expect(actions.cold.hasNodeDetailsFlyout()).toBeTruthy(); + }); + + test('shows default allocation warning when no node roles are found', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: {}, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.cold.hasDefaultAllocationWarning()).toBeTruthy(); + }); + + test('shows default allocation notice when warm or hot tiers exists, but not cold tier', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.cold.hasDefaultAllocationNotice()).toBeTruthy(); + }); + + test(`doesn't show default allocation notice when node with "data" role exists`, async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + await act(async () => { + testBed = await setup(); + }); + const { actions, component } = testBed; + + component.update(); + await actions.cold.enable(true); + + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.cold.hasDefaultAllocationNotice()).toBeFalsy(); + }); + }); + + describe('not on cloud', () => { + test('shows all allocation options, even if using legacy config', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + await act(async () => { + testBed = await setup(); + }); + const { actions, component, exists } = testBed; + + component.update(); + await actions.warm.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + // Assert that default, custom and 'none' options exist + await actions.warm.openNodeAttributesSection(); + expect(exists('defaultDataAllocationOption')).toBeTruthy(); + expect(exists('customDataAllocationOption')).toBeTruthy(); + expect(exists('noneDataAllocationOption')).toBeTruthy(); + }); + }); + + describe('on cloud', () => { + describe('with deprecated data role config', () => { + test('should hide data tier option on cloud using legacy node role configuration', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + // On cloud, if using legacy config there will not be any "data_*" roles set. + nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + const { actions, component, exists } = testBed; + + component.update(); + await actions.warm.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + // Assert that custom and 'none' options exist + await actions.warm.openNodeAttributesSection(); + expect(exists('defaultDataAllocationOption')).toBeFalsy(); + expect(exists('customDataAllocationOption')).toBeTruthy(); + expect(exists('noneDataAllocationOption')).toBeTruthy(); + }); + }); + + describe('with node role config', () => { + test('shows off, custom and data role options on cloud with data roles', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + const { actions, component, exists } = testBed; + + component.update(); + await actions.warm.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + await actions.warm.openNodeAttributesSection(); + expect(exists('defaultDataAllocationOption')).toBeTruthy(); + expect(exists('customDataAllocationOption')).toBeTruthy(); + expect(exists('noneDataAllocationOption')).toBeTruthy(); + // We should not be showing the call-to-action for users to activate data tiers in cloud + expect(exists('cloudDataTierCallout')).toBeFalsy(); + }); + + test('shows cloud notice when cold tier nodes do not exist', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + const { actions, component, exists } = testBed; + + component.update(); + await actions.cold.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + expect(exists('cloudDataTierCallout')).toBeTruthy(); + // Assert that other notices are not showing + expect(actions.cold.hasDefaultAllocationNotice()).toBeFalsy(); + expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy(); + }); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts new file mode 100644 index 0000000000000..9c23780f1d021 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts @@ -0,0 +1,143 @@ +/* + * Copyright 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 { act } from 'react-dom/test-utils'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { DEFAULT_POLICY } from '../constants'; + +describe(' reactive form', () => { + let testBed: EditPolicyTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([DEFAULT_POLICY]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + describe('rollover', () => { + test('shows forcemerge when rollover enabled', async () => { + const { actions } = testBed; + expect(actions.hot.forceMergeFieldExists()).toBeTruthy(); + }); + test('hides forcemerge when rollover is disabled', async () => { + const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + expect(actions.hot.forceMergeFieldExists()).toBeFalsy(); + }); + + test('shows shrink input when rollover enabled', async () => { + const { actions } = testBed; + expect(actions.hot.shrinkExists()).toBeTruthy(); + }); + test('hides shrink input when rollover is disabled', async () => { + const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + expect(actions.hot.shrinkExists()).toBeFalsy(); + }); + test('shows readonly input when rollover enabled', async () => { + const { actions } = testBed; + expect(actions.hot.readonlyExists()).toBeTruthy(); + }); + test('hides readonly input when rollover is disabled', async () => { + const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + expect(actions.hot.readonlyExists()).toBeFalsy(); + }); + }); + + describe('timing', () => { + test('warm phase shows timing only when enabled', async () => { + const { actions } = testBed; + expect(actions.warm.hasMinAgeInput()).toBeFalsy(); + await actions.warm.enable(true); + expect(actions.warm.hasMinAgeInput()).toBeTruthy(); + }); + + test('cold phase shows timing only when enabled', async () => { + const { actions } = testBed; + expect(actions.cold.hasMinAgeInput()).toBeFalsy(); + await actions.cold.enable(true); + expect(actions.cold.hasMinAgeInput()).toBeTruthy(); + }); + + test('delete phase shows timing after it was enabled', async () => { + const { actions } = testBed; + expect(actions.delete.hasMinAgeInput()).toBeFalsy(); + await actions.delete.enablePhase(); + expect(actions.delete.hasMinAgeInput()).toBeTruthy(); + }); + }); + + describe('delete phase', () => { + test('is hidden when disabled', async () => { + const { actions } = testBed; + expect(actions.delete.isShown()).toBeFalsy(); + await actions.delete.enablePhase(); + expect(actions.delete.isShown()).toBeTruthy(); + }); + }); + + describe('json in flyout', () => { + test('renders a json in flyout for a default policy', async () => { + const { find, component } = testBed; + await act(async () => { + find('requestButton').simulate('click'); + }); + component.update(); + + const json = component.find(`code`).text(); + const expected = `PUT _ilm/policy/my_policy\n${JSON.stringify( + { + policy: { + phases: { + hot: { + min_age: '0ms', + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + }, + }, + }, + }, + }, + null, + 2 + )}`; + expect(json).toBe(expected); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts index 823138aad13b9..6ef2b4c231ce1 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts @@ -7,7 +7,11 @@ import { fakeServer, SinonFakeServer } from 'sinon'; import { API_BASE_PATH } from '../../../common/constants'; -import { ListNodesRouteResponse, ListSnapshotReposResponse } from '../../../common/types'; +import { + ListNodesRouteResponse, + ListSnapshotReposResponse, + NodesDetailsResponse, +} from '../../../common/types'; export const init = () => { const server = fakeServer.create(); @@ -48,6 +52,14 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setNodesDetails = (nodeAttributes: string, body: NodesDetailsResponse) => { + server.respondWith('GET', `${API_BASE_PATH}/nodes/${nodeAttributes}/details`, [ + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + const setListSnapshotRepos = (body: ListSnapshotReposResponse) => { server.respondWith('GET', `${API_BASE_PATH}/snapshot_repositories`, [ 200, @@ -60,6 +72,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { setLoadPolicies, setLoadSnapshotPolicies, setListNodes, + setNodesDetails, setListSnapshotRepos, }; }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/README.md b/x-pack/plugins/index_lifecycle_management/__jest__/components/README.md deleted file mode 100644 index ce1ea7aa396a6..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Deprecated - -This test folder contains useful test coverage, mostly error states for form validation. However, it is -not in keeping with other ES UI maintained plugins. See ../client_integration for the established pattern -of tests. - -The tests here should be migrated to the above pattern and should not be added to. Any new test coverage must -be added to ../client_integration. diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx deleted file mode 100644 index 7c199e2ced765..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ /dev/null @@ -1,967 +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 React, { ReactElement } from 'react'; -import { act } from 'react-dom/test-utils'; -import moment from 'moment-timezone'; - -import { findTestSubject } from '@elastic/eui/lib/test'; -import { mountWithIntl } from '@kbn/test/jest'; -import { SinonFakeServer } from 'sinon'; -import { ReactWrapper } from 'enzyme'; -import axios from 'axios'; -import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import { createMemoryHistory } from 'history'; - -import { - notificationServiceMock, - fatalErrorsServiceMock, -} from '../../../../../src/core/public/mocks'; - -import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks'; - -import { CloudSetup } from '../../../cloud/public'; - -import { EditPolicy } from '../../public/application/sections/edit_policy/edit_policy'; -import { - EditPolicyContextProvider, - EditPolicyContextValue, -} from '../../public/application/sections/edit_policy/edit_policy_context'; - -import { KibanaContextProvider } from '../../public/shared_imports'; - -import { init as initHttp } from '../../public/application/services/http'; -import { init as initUiMetric } from '../../public/application/services/ui_metric'; -import { init as initNotification } from '../../public/application/services/notification'; -import { PolicyFromES } from '../../common/types'; - -import { i18nTexts } from '../../public/application/sections/edit_policy/i18n_texts'; -import { editPolicyHelpers } from './helpers'; -import { defaultPolicy } from '../../public/application/constants'; - -// @ts-ignore -initHttp(axios.create({ adapter: axiosXhrAdapter })); -initUiMetric(usageCollectionPluginMock.createSetupContract()); -initNotification( - notificationServiceMock.createSetupContract().toasts, - fatalErrorsServiceMock.createSetupContract() -); - -const history = createMemoryHistory(); -let server: SinonFakeServer; -let httpRequestsMockHelpers: editPolicyHelpers.EditPolicySetup['http']['httpRequestsMockHelpers']; -let http: editPolicyHelpers.EditPolicySetup['http']; -const policy = { - phases: { - hot: { - min_age: '0s', - actions: { - rollover: { - max_size: '1gb', - }, - }, - }, - }, -}; -const policies: PolicyFromES[] = []; -for (let i = 0; i < 105; i++) { - policies.push({ - version: i, - modified_date: moment().subtract(i, 'days').toISOString(), - linkedIndices: i % 2 === 0 ? [`index${i}`] : undefined, - name: `testy${i}`, - policy: { - ...policy, - name: `testy${i}`, - }, - }); -} -window.scrollTo = jest.fn(); - -jest.mock('@elastic/eui', () => { - const original = jest.requireActual('@elastic/eui'); - - return { - ...original, - EuiIcon: 'eui-icon', // using custom react-svg icon causes issues, mocking for now. - }; -}); - -let component: ReactElement; -const activatePhase = async (rendered: ReactWrapper, phase: string) => { - const testSubject = `enablePhaseSwitch-${phase}`; - await act(async () => { - await findTestSubject(rendered, testSubject).simulate('click'); - }); - rendered.update(); -}; -const activateDeletePhase = async (rendered: ReactWrapper) => { - const testSubject = `enableDeletePhaseButton`; - await act(async () => { - await findTestSubject(rendered, testSubject).simulate('click'); - }); - rendered.update(); -}; -const openNodeAttributesSection = async (rendered: ReactWrapper, phase: string) => { - const getControls = () => findTestSubject(rendered, `${phase}-dataTierAllocationControls`); - await act(async () => { - findTestSubject(getControls(), 'dataTierSelect').simulate('click'); - }); - rendered.update(); - await act(async () => { - findTestSubject(getControls(), 'customDataAllocationOption').simulate('click'); - }); - rendered.update(); -}; -const expectedErrorMessages = (rendered: ReactWrapper, expectedMessages: string[]) => { - const errorMessages = rendered.find('.euiFormErrorText'); - expect(errorMessages.length).toBe(expectedMessages.length); - expectedMessages.forEach((expectedErrorMessage) => { - let foundErrorMessage; - for (let i = 0; i < errorMessages.length; i++) { - if (errorMessages.at(i).text() === expectedErrorMessage) { - foundErrorMessage = true; - } - } - expect(foundErrorMessage).toBe(true); - }); -}; -const noDefaultRollover = async (rendered: ReactWrapper) => { - await act(async () => { - findTestSubject(rendered, 'useDefaultRolloverSwitch').simulate('click'); - }); - rendered.update(); -}; -const noRollover = async (rendered: ReactWrapper) => { - await noDefaultRollover(rendered); - await act(async () => { - findTestSubject(rendered, 'rolloverSwitch').simulate('click'); - }); - rendered.update(); -}; -const getNodeAttributeSelect = (rendered: ReactWrapper, phase: string) => { - return findTestSubject(rendered, `${phase}-selectedNodeAttrs`); -}; -const setPolicyName = async (rendered: ReactWrapper, policyName: string) => { - const policyNameField = findTestSubject(rendered, 'policyNameField'); - await act(async () => { - policyNameField.simulate('change', { target: { value: policyName } }); - }); - rendered.update(); -}; -const setPhaseAfter = async (rendered: ReactWrapper, phase: string, after: string | number) => { - const afterInput = findTestSubject(rendered, `${phase}-selectedMinimumAge`); - await act(async () => { - afterInput.simulate('change', { target: { value: after } }); - }); - rendered.update(); -}; -const setPhaseIndexPriority = async ( - rendered: ReactWrapper, - phase: string, - priority: string | number -) => { - const priorityInput = findTestSubject(rendered, `${phase}-indexPriority`); - await act(async () => { - priorityInput.simulate('change', { target: { value: priority } }); - }); - rendered.update(); -}; -const save = async (rendered: ReactWrapper) => { - const saveButton = findTestSubject(rendered, 'savePolicyButton'); - await act(async () => { - saveButton.simulate('click'); - }); - rendered.update(); -}; - -const MyComponent = ({ - isCloudEnabled, - isNewPolicy, - policy: _policy, - existingPolicies, - getUrlForApp, - policyName, -}: EditPolicyContextValue & { isCloudEnabled: boolean }) => { - return ( - - true, - }, - }} - > - - - - ); -}; - -describe('edit policy', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); - afterAll(() => { - jest.useRealTimers(); - }); - - /** - * The form lib has a short delay (setTimeout) before running and rendering - * any validation errors. This helper advances timers and can trigger component - * state changes. - */ - const waitForFormLibValidation = (rendered: ReactWrapper) => { - act(() => { - jest.runAllTimers(); - }); - rendered.update(); - }; - - beforeEach(() => { - component = ( - true }} - /> - ); - - ({ http } = editPolicyHelpers.setup()); - ({ server, httpRequestsMockHelpers } = http); - - httpRequestsMockHelpers.setPoliciesResponse(policies); - }); - describe('top level form', () => { - test('should show error when trying to save empty form', async () => { - const rendered = mountWithIntl(component); - await save(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.policyNameRequiredMessage]); - }); - test('should show error when trying to save policy name with space', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'my policy'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.policyNameContainsInvalidChars]); - }); - test('should show error when trying to save policy name that is already used', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'testy0'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [ - i18nTexts.editPolicy.errors.policyNameAlreadyUsedErrorMessage, - ]); - }); - test('should show error when trying to save as new policy but using the same name', async () => { - component = ( - true }} - /> - ); - const rendered = mountWithIntl(component); - findTestSubject(rendered, 'saveAsNewSwitch').simulate('click'); - rendered.update(); - await setPolicyName(rendered, 'testy0'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [ - i18nTexts.editPolicy.errors.policyNameAlreadyUsedErrorMessage, - ]); - }); - test('should show error when trying to save policy name with comma', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'my,policy'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.policyNameContainsInvalidChars]); - }); - test('should show error when trying to save policy name starting with underscore', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, '_mypolicy'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [ - i18nTexts.editPolicy.errors.policyNameStartsWithUnderscoreErrorMessage, - ]); - }); - test('should show correct json in policy flyout', async () => { - const rendered = mountWithIntl( - true }} - /> - ); - - await act(async () => { - findTestSubject(rendered, 'requestButton').simulate('click'); - }); - rendered.update(); - - const json = rendered.find(`code`).text(); - const expected = `PUT _ilm/policy/my-policy\n${JSON.stringify( - { - policy: { - phases: { - hot: { - actions: { - rollover: { - max_age: '30d', - max_size: '50gb', - }, - }, - min_age: '0ms', - }, - }, - }, - }, - null, - 2 - )}`; - expect(json).toBe(expected); - }); - }); - describe('hot phase', () => { - test('should show errors when trying to save with no max size, no max age and no max docs', async () => { - const rendered = mountWithIntl(component); - await noDefaultRollover(rendered); - expect(findTestSubject(rendered, 'rolloverSettingsRequired').exists()).toBeFalsy(); - await setPolicyName(rendered, 'mypolicy'); - const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored'); - await act(async () => { - maxSizeInput.simulate('change', { target: { value: '' } }); - }); - waitForFormLibValidation(rendered); - const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge'); - await act(async () => { - maxAgeInput.simulate('change', { target: { value: '' } }); - }); - waitForFormLibValidation(rendered); - const maxDocsInput = findTestSubject(rendered, 'hot-selectedMaxDocuments'); - await act(async () => { - maxDocsInput.simulate('change', { target: { value: '' } }); - }); - waitForFormLibValidation(rendered); - await save(rendered); - expect(findTestSubject(rendered, 'rolloverSettingsRequired').exists()).toBeTruthy(); - }); - test('should show number above 0 required error when trying to save with -1 for max size', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - await noDefaultRollover(rendered); - const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored'); - await act(async () => { - maxSizeInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - rendered.update(); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show number above 0 required error when trying to save with 0 for max size', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - await noDefaultRollover(rendered); - const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored'); - await act(async () => { - maxSizeInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show number above 0 required error when trying to save with -1 for max age', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - await noDefaultRollover(rendered); - const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge'); - await act(async () => { - maxAgeInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show number above 0 required error when trying to save with 0 for max age', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - await noDefaultRollover(rendered); - const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge'); - await act(async () => { - maxAgeInput.simulate('change', { target: { value: '0' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show forcemerge input when rollover enabled', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - expect(findTestSubject(rendered, 'hot-forceMergeSwitch').exists()).toBeTruthy(); - }); - test('should hide forcemerge input when rollover is disabled', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - await noRollover(rendered); - waitForFormLibValidation(rendered); - expect(findTestSubject(rendered, 'hot-forceMergeSwitch').exists()).toBeFalsy(); - }); - test('should show positive number required above zero error when trying to save hot phase with 0 for force merge', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - act(() => { - findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click'); - }); - rendered.update(); - const forcemergeInput = findTestSubject(rendered, 'hot-selectedForceMergeSegments'); - await act(async () => { - forcemergeInput.simulate('change', { target: { value: '0' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show positive number above 0 required error when trying to save hot phase with -1 for force merge', async () => { - const rendered = mountWithIntl(component); - await setPolicyName(rendered, 'mypolicy'); - findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click'); - rendered.update(); - const forcemergeInput = findTestSubject(rendered, 'hot-selectedForceMergeSegments'); - await act(async () => { - forcemergeInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - await save(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show positive number required error when trying to save with -1 for index priority', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - - await setPhaseIndexPriority(rendered, 'hot', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - - test("doesn't show min age input", async () => { - const rendered = mountWithIntl(component); - expect(findTestSubject(rendered, 'hot-selectedMinimumAge').exists()).toBeFalsy(); - }); - }); - describe('warm phase', () => { - beforeEach(() => { - server.respondImmediately = true; - http.setupNodeListResponse(); - httpRequestsMockHelpers.setNodesDetailsResponse('attribute:true', [ - { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, - ]); - }); - - test('should show number required error when trying to save empty warm phase', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', ''); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - test('should allow 0 for phase timing', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '0'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, []); - }); - test('should show positive number required error when trying to save warm phase with -1 for after', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - test('should show positive number required error when trying to save warm phase with -1 for index priority', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '1'); - await setPhaseAfter(rendered, 'warm', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - test('should show positive number required above zero error when trying to save warm phase with 0 for shrink', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - act(() => { - findTestSubject(rendered, 'warm-shrinkSwitch').simulate('click'); - }); - rendered.update(); - await setPhaseAfter(rendered, 'warm', '1'); - const shrinkInput = findTestSubject(rendered, 'warm-primaryShardCount'); - await act(async () => { - shrinkInput.simulate('change', { target: { value: '0' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show positive number above 0 required error when trying to save warm phase with -1 for shrink', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '1'); - act(() => { - findTestSubject(rendered, 'warm-shrinkSwitch').simulate('click'); - }); - rendered.update(); - const shrinkInput = findTestSubject(rendered, 'warm-primaryShardCount'); - await act(async () => { - shrinkInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show positive number required above zero error when trying to save warm phase with 0 for force merge', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '1'); - act(() => { - findTestSubject(rendered, 'warm-forceMergeSwitch').simulate('click'); - }); - rendered.update(); - const forcemergeInput = findTestSubject(rendered, 'warm-selectedForceMergeSegments'); - await act(async () => { - forcemergeInput.simulate('change', { target: { value: '0' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show positive number above 0 required error when trying to save warm phase with -1 for force merge', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - await setPhaseAfter(rendered, 'warm', '1'); - await act(async () => { - findTestSubject(rendered, 'warm-forceMergeSwitch').simulate('click'); - }); - rendered.update(); - const forcemergeInput = findTestSubject(rendered, 'warm-selectedForceMergeSegments'); - await act(async () => { - forcemergeInput.simulate('change', { target: { value: '-1' } }); - }); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]); - }); - test('should show spinner for node attributes input when loading', async () => { - server.respondImmediately = false; - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'warm-dataTierAllocationControls').exists()).toBeTruthy(); - expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); - expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy(); - }); - test('should show warning instead of node attributes input when none exist', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data: ['node1'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'warm'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy(); - expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy(); - }); - test('should show node attributes input when attributes exist', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'warm'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm'); - expect(nodeAttributesSelect.exists()).toBeTruthy(); - expect(nodeAttributesSelect.find('option').length).toBe(2); - }); - test('should show view node attributes link when attribute selected and show flyout when clicked', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'warm'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'warm'); - expect(nodeAttributesSelect.exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'warm-viewNodeDetailsFlyoutButton').exists()).toBeFalsy(); - expect(nodeAttributesSelect.find('option').length).toBe(2); - await act(async () => { - nodeAttributesSelect.simulate('change', { target: { value: 'attribute:true' } }); - }); - rendered.update(); - const flyoutButton = findTestSubject(rendered, 'warm-viewNodeDetailsFlyoutButton'); - expect(flyoutButton.exists()).toBeTruthy(); - await act(async () => { - await flyoutButton.simulate('click'); - }); - rendered.update(); - expect(rendered.find('.euiFlyout').exists()).toBeTruthy(); - }); - test('should show default allocation warning when no node roles are found', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: {}, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy(); - }); - test('should show default allocation notice when hot tier exists, but not warm tier', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data_hot: ['test'], data_cold: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeTruthy(); - }); - test('should not show default allocation notice when node with "data" role exists', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy(); - }); - - test('shows min age input only when enabled', async () => { - const rendered = mountWithIntl(component); - expect(findTestSubject(rendered, 'warm-selectedMinimumAge').exists()).toBeFalsy(); - await activatePhase(rendered, 'warm'); - expect(findTestSubject(rendered, 'warm-selectedMinimumAge').exists()).toBeTruthy(); - }); - }); - describe('cold phase', () => { - beforeEach(() => { - server.respondImmediately = true; - http.setupNodeListResponse(); - httpRequestsMockHelpers.setNodesDetailsResponse('attribute:true', [ - { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, - ]); - }); - test('should allow 0 for phase timing', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - await setPhaseAfter(rendered, 'cold', '0'); - waitForFormLibValidation(rendered); - rendered.update(); - expectedErrorMessages(rendered, []); - }); - test('should show positive number required error when trying to save cold phase with -1 for after', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - await setPhaseAfter(rendered, 'cold', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - test('should show spinner for node attributes input when loading', async () => { - server.respondImmediately = false; - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'cold-dataTierAllocationControls').exists()).toBeTruthy(); - expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy(); - expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy(); - }); - test('should show warning instead of node attributes input when none exist', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data: ['node1'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'cold'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeTruthy(); - expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy(); - }); - test('should show node attributes input when attributes exist', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'cold'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold'); - expect(nodeAttributesSelect.exists()).toBeTruthy(); - expect(nodeAttributesSelect.find('option').length).toBe(2); - }); - test('should show view node attributes link when attribute selected and show flyout when clicked', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - await openNodeAttributesSection(rendered, 'cold'); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - const nodeAttributesSelect = getNodeAttributeSelect(rendered, 'cold'); - expect(nodeAttributesSelect.exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'cold-viewNodeDetailsFlyoutButton').exists()).toBeFalsy(); - expect(nodeAttributesSelect.find('option').length).toBe(2); - nodeAttributesSelect.simulate('change', { target: { value: 'attribute:true' } }); - rendered.update(); - const flyoutButton = findTestSubject(rendered, 'cold-viewNodeDetailsFlyoutButton'); - expect(flyoutButton.exists()).toBeTruthy(); - await act(async () => { - await flyoutButton.simulate('click'); - }); - rendered.update(); - expect(rendered.find('.euiFlyout').exists()).toBeTruthy(); - }); - test('should show positive number required error when trying to save with -1 for index priority', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - await setPhaseAfter(rendered, 'cold', '1'); - await setPhaseIndexPriority(rendered, 'cold', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - test('should show default allocation warning when no node roles are found', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: {}, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy(); - }); - test('should show default allocation notice when warm or hot tiers exists, but not cold tier', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data_hot: ['test'], data_warm: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeTruthy(); - }); - test('should not show default allocation notice when node with "data" role exists', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy(); - }); - - test('shows min age input only when enabled', async () => { - const rendered = mountWithIntl(component); - expect(findTestSubject(rendered, 'cold-selectedMinimumAge').exists()).toBeFalsy(); - await activatePhase(rendered, 'cold'); - expect(findTestSubject(rendered, 'cold-selectedMinimumAge').exists()).toBeTruthy(); - }); - }); - describe('delete phase', () => { - test('should allow 0 for phase timing', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activateDeletePhase(rendered); - await setPhaseAfter(rendered, 'delete', '0'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, []); - }); - test('should show positive number required error when trying to save delete phase with -1 for after', async () => { - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activateDeletePhase(rendered); - await setPhaseAfter(rendered, 'delete', '-1'); - waitForFormLibValidation(rendered); - expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - - test('is hidden when disabled', async () => { - const rendered = mountWithIntl(component); - expect(findTestSubject(rendered, 'delete-phaseContent').exists()).toBeFalsy(); - await activateDeletePhase(rendered); - expect(findTestSubject(rendered, 'delete-phaseContent').exists()).toBeTruthy(); - }); - }); - describe('not on cloud', () => { - beforeEach(() => { - server.respondImmediately = true; - }); - test('should show all allocation options, even if using legacy config', async () => { - http.setupNodeListResponse({ - nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, - isUsingDeprecatedDataRoleConfig: true, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - - // Assert that default, custom and 'none' options exist - findTestSubject(rendered, 'dataTierSelect').simulate('click'); - expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy(); - }); - }); - describe('on cloud', () => { - beforeEach(() => { - component = ( - true }} - /> - ); - ({ http } = editPolicyHelpers.setup()); - ({ server, httpRequestsMockHelpers } = http); - server.respondImmediately = true; - - httpRequestsMockHelpers.setPoliciesResponse(policies); - }); - - describe('with deprecated data role config', () => { - test('should hide data tier option on cloud using legacy node role configuration', async () => { - http.setupNodeListResponse({ - nodesByAttributes: { test: ['123'] }, - // On cloud, if using legacy config there will not be any "data_*" roles set. - nodesByRoles: { data: ['test'] }, - isUsingDeprecatedDataRoleConfig: true, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - - // Assert that default, custom and 'none' options exist - findTestSubject(rendered, 'dataTierSelect').simulate('click'); - expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy(); - }); - }); - - describe('with node role config', () => { - test('should show off, custom and data role options on cloud with data roles', async () => { - http.setupNodeListResponse({ - nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'warm'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - - findTestSubject(rendered, 'dataTierSelect').simulate('click'); - expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); - expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy(); - // We should not be showing the call-to-action for users to activate data tiers in cloud - expect(findTestSubject(rendered, 'cloudDataTierCallout').exists()).toBeFalsy(); - }); - - test('should show cloud notice when cold tier nodes do not exist', async () => { - http.setupNodeListResponse({ - nodesByAttributes: {}, - nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - const rendered = mountWithIntl(component); - await noRollover(rendered); - await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'cold'); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'cloudDataTierCallout').exists()).toBeTruthy(); - // Assert that other notices are not showing - expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy(); - expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); - }); - }); - }); -}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts deleted file mode 100644 index 49fd651ca9453..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/edit_policy.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { init as initHttpRequests } from './http_requests'; - -export type EditPolicySetup = ReturnType; - -export const setup = () => { - const { httpRequestsMockHelpers, server } = initHttpRequests(); - - const setupNodeListResponse = ( - response: Record = { - nodesByAttributes: { 'attribute:true': ['node1'] }, - nodesByRoles: { data: ['node1'] }, - } - ) => { - httpRequestsMockHelpers.setNodesListResponse(response); - }; - - return { - http: { - setupNodeListResponse, - httpRequestsMockHelpers, - server, - }, - }; -}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts deleted file mode 100644 index ea6e2af87a6d9..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts +++ /dev/null @@ -1,60 +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 sinon, { SinonFakeServer } from 'sinon'; - -export type HttpResponse = Record | any[]; - -const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { - const setPoliciesResponse = (response: HttpResponse = []) => { - server.respondWith('/api/index_lifecycle_management/policies', [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(response), - ]); - }; - - const setNodesListResponse = (response: HttpResponse = []) => { - server.respondWith('/api/index_lifecycle_management/nodes/list', [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(response), - ]); - }; - - const setNodesDetailsResponse = (nodeAttributes: string, response: HttpResponse = []) => { - server.respondWith(`/api/index_lifecycle_management/nodes/${nodeAttributes}/details`, [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(response), - ]); - }; - - return { - setPoliciesResponse, - setNodesListResponse, - setNodesDetailsResponse, - }; -}; - -export type HttpRequestMockHelpers = ReturnType; - -export const init = () => { - const server = sinon.fakeServer.create(); - - // Define default response for unhandled requests. - // We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry, - // and we can mock them all with a 200 instead of mocking each one individually. - server.respondWith([200, {}, 'DefaultSinonMockServerResponse']); - - const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server); - - return { - server, - httpRequestsMockHelpers, - }; -}; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts deleted file mode 100644 index 95a45d12e23a2..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/index.ts +++ /dev/null @@ -1,12 +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 * as editPolicyHelpers from './edit_policy'; - -export { HttpRequestMockHelpers, init } from './http_requests'; - -export { editPolicyHelpers }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx similarity index 92% rename from x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx rename to x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx index 803560c67cf28..7733d547e3472 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/policy_table.test.tsx @@ -15,14 +15,14 @@ import { fatalErrorsServiceMock, injectedMetadataServiceMock, scopedHistoryMock, -} from '../../../../../src/core/public/mocks'; -import { HttpService } from '../../../../../src/core/public/http'; -import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks'; +} from '../../../../src/core/public/mocks'; +import { HttpService } from '../../../../src/core/public/http'; +import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/public/mocks'; -import { PolicyFromES } from '../../common/types'; -import { PolicyTable } from '../../public/application/sections/policy_table/policy_table'; -import { init as initHttp } from '../../public/application/services/http'; -import { init as initUiMetric } from '../../public/application/services/ui_metric'; +import { PolicyFromES } from '../common/types'; +import { PolicyTable } from '../public/application/sections/policy_table/policy_table'; +import { init as initHttp } from '../public/application/services/http'; +import { init as initUiMetric } from '../public/application/services/ui_metric'; initHttp( new HttpService().setup({ diff --git a/x-pack/plugins/index_lifecycle_management/common/types/api.ts b/x-pack/plugins/index_lifecycle_management/common/types/api.ts index 81190acd01ad1..6d4e11c58f9bb 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/api.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/api.ts @@ -20,6 +20,16 @@ export interface ListNodesRouteResponse { isUsingDeprecatedDataRoleConfig: boolean; } +export interface NodesDetails { + nodeId: string; + stats: { + name: string; + host: string; + }; +} + +export type NodesDetailsResponse = NodesDetails[]; + export interface ListSnapshotReposResponse { /** * An array of repository names From 1fa742d0ceeb543fae005bc576318f97e85df2a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 19 Feb 2021 08:57:14 -0500 Subject: [PATCH 05/32] [APM] Kql Search Bar suggests values outside the selected time range (#91918) --- .../apm/server/lib/index_pattern/get_dynamic_index_pattern.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index cb6183510ad16..8b81101fd2f39 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -15,6 +15,7 @@ import { withApmSpan } from '../../utils/with_apm_span'; export interface IndexPatternTitleAndFields { title: string; + timeFieldName: string; fields: FieldDescriptor[]; } @@ -52,6 +53,7 @@ export const getDynamicIndexPattern = ({ const indexPattern: IndexPatternTitleAndFields = { fields, + timeFieldName: '@timestamp', title: indexPatternTitle, }; From 8b909cedc822569911cd794a35e172e11998dae6 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Fri, 19 Feb 2021 14:05:47 +0000 Subject: [PATCH 06/32] [Search Source] Do not request unmapped fields if source filters are provided (#91921) --- .../data/common/search/search_source/search_source.test.ts | 5 +---- .../data/common/search/search_source/search_source.ts | 7 +------ 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index 030e620bea34b..fd97a3d3381a9 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -418,10 +418,7 @@ describe('SearchSource', () => { searchSource.setField('fields', [{ field: '*', include_unmapped: 'true' }]); const request = await searchSource.getSearchRequestBody(); - expect(request.fields).toEqual([ - { field: 'field1', include_unmapped: 'true' }, - { field: 'field2', include_unmapped: 'true' }, - ]); + expect(request.fields).toEqual([{ field: 'field1' }, { field: 'field2' }]); }); test('returns all scripted fields when one fields entry is *', async () => { diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 118bb04c1742b..486f2b3667453 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -503,12 +503,7 @@ export class SearchSource { // we need to get the list of fields from an index pattern return fields .filter((fld: IndexPatternField) => filterSourceFields(fld.name)) - .map((fld: IndexPatternField) => ({ - field: fld.name, - ...((wildcardField as Record)?.include_unmapped && { - include_unmapped: (wildcardField as Record).include_unmapped, - }), - })); + .map((fld: IndexPatternField) => ({ field: fld.name })); } private getFieldFromDocValueFieldsOrIndexPattern( From 4d34a13babd74d0c43066993468d4c69527254d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Fri, 19 Feb 2021 15:06:33 +0100 Subject: [PATCH 07/32] [Logs UI] Replace dependencies in the infra bundle (#91503) --- packages/kbn-optimizer/limits.yml | 2 +- x-pack/plugins/infra/common/formatters/index.ts | 3 +-- x-pack/plugins/infra/public/apps/legacy_app.tsx | 10 ++++------ x-pack/plugins/infra/public/hooks/use_link_props.tsx | 11 +++++------ 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 1a157624d7a8a..1ebd0a9b83bd0 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -34,7 +34,7 @@ pageLoadAssetSize: indexLifecycleManagement: 107090 indexManagement: 140608 indexPatternManagement: 28222 - infra: 204800 + infra: 184320 fleet: 415829 ingestPipelines: 58003 inputControlVis: 172675 diff --git a/x-pack/plugins/infra/common/formatters/index.ts b/x-pack/plugins/infra/common/formatters/index.ts index 61e01aa7e6837..a4aeee8084824 100644 --- a/x-pack/plugins/infra/common/formatters/index.ts +++ b/x-pack/plugins/infra/common/formatters/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import Mustache from 'mustache'; import { createBytesFormatter } from './bytes'; import { formatNumber } from './number'; import { formatPercent } from './percent'; @@ -34,5 +33,5 @@ export const createFormatter = (format: InventoryFormatterType, template: string } const fmtFn = FORMATTERS[format]; const value = fmtFn(Number(val)); - return Mustache.render(template, { value }); + return template.replace(/{{value}}/g, value); }; diff --git a/x-pack/plugins/infra/public/apps/legacy_app.tsx b/x-pack/plugins/infra/public/apps/legacy_app.tsx index 50f24c2042c13..8aeb99c426651 100644 --- a/x-pack/plugins/infra/public/apps/legacy_app.tsx +++ b/x-pack/plugins/infra/public/apps/legacy_app.tsx @@ -11,7 +11,6 @@ import { AppMountParameters } from 'kibana/public'; import React from 'react'; import ReactDOM from 'react-dom'; import { Route, RouteProps, Router, Switch } from 'react-router-dom'; -import url from 'url'; // This exists purely to facilitate legacy app/infra URL redirects. // It will be removed in 8.0.0. @@ -79,11 +78,10 @@ const LegacyApp: React.FunctionComponent<{ history: History }> = ({ his nextPath = nextPathParts[0]; nextSearch = nextPathParts[1] ? nextPathParts[1] : undefined; - let nextUrl = url.format({ - pathname: `${nextBasePath}/${nextPath}`, - hash: undefined, - search: nextSearch, - }); + const builtPathname = `${nextBasePath}/${nextPath}`; + const builtSearch = nextSearch ? `?${nextSearch}` : ''; + + let nextUrl = `${builtPathname}${builtSearch}`; nextUrl = nextUrl.replace('//', '/'); diff --git a/x-pack/plugins/infra/public/hooks/use_link_props.tsx b/x-pack/plugins/infra/public/hooks/use_link_props.tsx index 72a538cd56281..7546f9f0c9f79 100644 --- a/x-pack/plugins/infra/public/hooks/use_link_props.tsx +++ b/x-pack/plugins/infra/public/hooks/use_link_props.tsx @@ -7,7 +7,6 @@ import { useMemo } from 'react'; import { stringify } from 'query-string'; -import url from 'url'; import { url as urlUtils } from '../../../../../src/plugins/kibana_utils/public'; import { usePrefixPathWithBasepath } from './use_prefix_path_with_basepath'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; @@ -58,11 +57,11 @@ export const useLinkProps = ( }, [pathname, encodedSearch]); const href = useMemo(() => { - const link = url.format({ - pathname, - hash: mergedHash, - search: !hash ? encodedSearch : undefined, - }); + const builtPathname = pathname ?? ''; + const builtHash = mergedHash ? `#${mergedHash}` : ''; + const builtSearch = !hash ? (encodedSearch ? `?${encodedSearch}` : '') : ''; + + const link = `${builtPathname}${builtSearch}${builtHash}`; return prefixer(app, link); }, [mergedHash, hash, encodedSearch, pathname, prefixer, app]); From eea6f82e4e436abdf28266127a801a57e3c2dcad Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Fri, 19 Feb 2021 08:00:01 -0700 Subject: [PATCH 08/32] [Maps] Use new index patterns service for Maps telemetry (#86703) Co-authored-by: Matt Kime --- ...rver.indexpatternsservice._constructor_.md | 20 +++ ...-server.indexpatternsservice.clearcache.md | 13 ++ ...data-server.indexpatternsservice.create.md | 27 +++ ...rver.indexpatternsservice.createandsave.md | 26 +++ ....indexpatternsservice.createsavedobject.md | 25 +++ ...data-server.indexpatternsservice.delete.md | 24 +++ ...tternsservice.ensuredefaultindexpattern.md | 11 ++ ...er.indexpatternsservice.fieldarraytomap.md | 13 ++ ...s-data-server.indexpatternsservice.find.md | 13 ++ ...ns-data-server.indexpatternsservice.get.md | 13 ++ ...ta-server.indexpatternsservice.getcache.md | 11 ++ ...-server.indexpatternsservice.getdefault.md | 13 ++ ...atternsservice.getfieldsforindexpattern.md | 13 ++ ...dexpatternsservice.getfieldsforwildcard.md | 13 ++ ...data-server.indexpatternsservice.getids.md | 13 ++ ...er.indexpatternsservice.getidswithtitle.md | 16 ++ ...a-server.indexpatternsservice.gettitles.md | 13 ++ ...lugins-data-server.indexpatternsservice.md | 35 +++- ...rver.indexpatternsservice.refreshfields.md | 13 ++ ....indexpatternsservice.savedobjecttospec.md | 13 ++ ...-server.indexpatternsservice.setdefault.md | 13 ++ ....indexpatternsservice.updatesavedobject.md | 26 +++ ...ata-server.indexpatternsserviceprovider.md | 19 ++ ...ver.indexpatternsserviceprovider.setup.md} | 4 +- ...ver.indexpatternsserviceprovider.start.md} | 4 +- .../kibana-plugin-plugins-data-server.md | 1 + ...plugin-plugins-data-server.plugin.start.md | 4 +- src/plugins/data/server/index.ts | 4 +- .../index_patterns/index_patterns_service.ts | 2 +- src/plugins/data/server/server.api.md | 78 +++++++-- .../maps/server/kibana_server_services.ts | 18 +- .../maps_telemetry/maps_telemetry.test.js | 61 ++++++- .../server/maps_telemetry/maps_telemetry.ts | 164 +++++++----------- .../sample_index_pattern_saved_objects.json | 59 ------- x-pack/plugins/maps/server/plugin.ts | 13 +- 35 files changed, 616 insertions(+), 192 deletions(-) create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.create.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.delete.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.find.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.get.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getids.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md rename docs/development/plugins/data/server/{kibana-plugin-plugins-data-server.indexpatternsservice.setup.md => kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md} (67%) rename docs/development/plugins/data/server/{kibana-plugin-plugins-data-server.indexpatternsservice.start.md => kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md} (75%) delete mode 100644 x-pack/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md new file mode 100644 index 0000000000000..86e879eecc5a9 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [(constructor)](./kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md) + +## IndexPatternsService.(constructor) + +Constructs a new instance of the `IndexPatternsService` class + +Signature: + +```typescript +constructor({ uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, }: IndexPatternsServiceDeps); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, } | IndexPatternsServiceDeps | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md new file mode 100644 index 0000000000000..eb0e92f3760c8 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [clearCache](./kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md) + +## IndexPatternsService.clearCache property + +Clear index pattern list cache + +Signature: + +```typescript +clearCache: (id?: string | undefined) => void; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.create.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.create.md new file mode 100644 index 0000000000000..e5cc7c2e433ca --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.create.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [create](./kibana-plugin-plugins-data-server.indexpatternsservice.create.md) + +## IndexPatternsService.create() method + +Create a new index pattern instance + +Signature: + +```typescript +create(spec: IndexPatternSpec, skipFetchFields?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| spec | IndexPatternSpec | | +| skipFetchFields | boolean | | + +Returns: + +`Promise` + +IndexPattern + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md new file mode 100644 index 0000000000000..9b6e3a82528d5 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [createAndSave](./kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md) + +## IndexPatternsService.createAndSave() method + +Create a new index pattern and save it right away + +Signature: + +```typescript +createAndSave(spec: IndexPatternSpec, override?: boolean, skipFetchFields?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| spec | IndexPatternSpec | | +| override | boolean | | +| skipFetchFields | boolean | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md new file mode 100644 index 0000000000000..6ffadf648f5b6 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [createSavedObject](./kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md) + +## IndexPatternsService.createSavedObject() method + +Save a new index pattern + +Signature: + +```typescript +createSavedObject(indexPattern: IndexPattern, override?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPattern | IndexPattern | | +| override | boolean | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.delete.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.delete.md new file mode 100644 index 0000000000000..929a803849428 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.delete.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [delete](./kibana-plugin-plugins-data-server.indexpatternsservice.delete.md) + +## IndexPatternsService.delete() method + +Deletes an index pattern from .kibana index + +Signature: + +```typescript +delete(indexPatternId: string): Promise<{}>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPatternId | string | | + +Returns: + +`Promise<{}>` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md new file mode 100644 index 0000000000000..c4f6b61e4feb4 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [ensureDefaultIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md) + +## IndexPatternsService.ensureDefaultIndexPattern property + +Signature: + +```typescript +ensureDefaultIndexPattern: EnsureDefaultIndexPattern; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md new file mode 100644 index 0000000000000..e0b27c317ff74 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [fieldArrayToMap](./kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md) + +## IndexPatternsService.fieldArrayToMap property + +Converts field array to map + +Signature: + +```typescript +fieldArrayToMap: (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.find.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.find.md new file mode 100644 index 0000000000000..35b94133462aa --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.find.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [find](./kibana-plugin-plugins-data-server.indexpatternsservice.find.md) + +## IndexPatternsService.find property + +Find and load index patterns by title + +Signature: + +```typescript +find: (search: string, size?: number) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.get.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.get.md new file mode 100644 index 0000000000000..874f1d1a490c7 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [get](./kibana-plugin-plugins-data-server.indexpatternsservice.get.md) + +## IndexPatternsService.get property + +Get an index pattern by id. Cache optimized + +Signature: + +```typescript +get: (id: string) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md new file mode 100644 index 0000000000000..821c06984e55e --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getCache](./kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md) + +## IndexPatternsService.getCache property + +Signature: + +```typescript +getCache: () => Promise[] | null | undefined>; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md new file mode 100644 index 0000000000000..104e605e01bcb --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md) + +## IndexPatternsService.getDefault property + +Get default index pattern + +Signature: + +```typescript +getDefault: () => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md new file mode 100644 index 0000000000000..db871c0bec83c --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getFieldsForIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md) + +## IndexPatternsService.getFieldsForIndexPattern property + +Get field list by providing an index patttern (or spec) + +Signature: + +```typescript +getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md new file mode 100644 index 0000000000000..0b2c6dbfdef8b --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getFieldsForWildcard](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md) + +## IndexPatternsService.getFieldsForWildcard property + +Get field list by providing { pattern } + +Signature: + +```typescript +getFieldsForWildcard: (options: GetFieldsOptions) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getids.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getids.md new file mode 100644 index 0000000000000..2f0fb56cc4457 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getids.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getIds](./kibana-plugin-plugins-data-server.indexpatternsservice.getids.md) + +## IndexPatternsService.getIds property + +Get list of index pattern ids + +Signature: + +```typescript +getIds: (refresh?: boolean) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md new file mode 100644 index 0000000000000..6433c78483545 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getIdsWithTitle](./kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md) + +## IndexPatternsService.getIdsWithTitle property + +Get list of index pattern ids with titles + +Signature: + +```typescript +getIdsWithTitle: (refresh?: boolean) => Promise>; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md new file mode 100644 index 0000000000000..385e7f70d237a --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getTitles](./kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md) + +## IndexPatternsService.getTitles property + +Get list of index pattern titles + +Signature: + +```typescript +getTitles: (refresh?: boolean) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md index 83e912d80dbd1..d55a6e9b325a2 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md @@ -7,13 +7,42 @@ Signature: ```typescript -export declare class IndexPatternsServiceProvider implements Plugin +export declare class IndexPatternsService ``` +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)({ uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, })](./kibana-plugin-plugins-data-server.indexpatternsservice._constructor_.md) | | Constructs a new instance of the IndexPatternsService class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [clearCache](./kibana-plugin-plugins-data-server.indexpatternsservice.clearcache.md) | | (id?: string | undefined) => void | Clear index pattern list cache | +| [ensureDefaultIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.ensuredefaultindexpattern.md) | | EnsureDefaultIndexPattern | | +| [fieldArrayToMap](./kibana-plugin-plugins-data-server.indexpatternsservice.fieldarraytomap.md) | | (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record<string, FieldSpec> | Converts field array to map | +| [find](./kibana-plugin-plugins-data-server.indexpatternsservice.find.md) | | (search: string, size?: number) => Promise<IndexPattern[]> | Find and load index patterns by title | +| [get](./kibana-plugin-plugins-data-server.indexpatternsservice.get.md) | | (id: string) => Promise<IndexPattern> | Get an index pattern by id. Cache optimized | +| [getCache](./kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<IndexPatternSavedObjectAttrs>[] | null | undefined> | | +| [getDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md) | | () => Promise<IndexPattern | null> | Get default index pattern | +| [getFieldsForIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md) | | (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise<any> | Get field list by providing an index patttern (or spec) | +| [getFieldsForWildcard](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md) | | (options: GetFieldsOptions) => Promise<any> | Get field list by providing { pattern } | +| [getIds](./kibana-plugin-plugins-data-server.indexpatternsservice.getids.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern ids | +| [getIdsWithTitle](./kibana-plugin-plugins-data-server.indexpatternsservice.getidswithtitle.md) | | (refresh?: boolean) => Promise<Array<{
id: string;
title: string;
}>> | Get list of index pattern ids with titles | +| [getTitles](./kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern titles | +| [refreshFields](./kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md) | | (indexPattern: IndexPattern) => Promise<void> | Refresh field list for a given index pattern | +| [savedObjectToSpec](./kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md) | | (savedObject: SavedObject<IndexPatternAttributes>) => IndexPatternSpec | Converts index pattern saved object to index pattern spec | +| [setDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md) | | (id: string, force?: boolean) => Promise<void> | Optionally set default index pattern, unless force = true | + ## Methods | Method | Modifiers | Description | | --- | --- | --- | -| [setup(core, { expressions })](./kibana-plugin-plugins-data-server.indexpatternsservice.setup.md) | | | -| [start(core, { fieldFormats, logger })](./kibana-plugin-plugins-data-server.indexpatternsservice.start.md) | | | +| [create(spec, skipFetchFields)](./kibana-plugin-plugins-data-server.indexpatternsservice.create.md) | | Create a new index pattern instance | +| [createAndSave(spec, override, skipFetchFields)](./kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md) | | Create a new index pattern and save it right away | +| [createSavedObject(indexPattern, override)](./kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md) | | Save a new index pattern | +| [delete(indexPatternId)](./kibana-plugin-plugins-data-server.indexpatternsservice.delete.md) | | Deletes an index pattern from .kibana index | +| [updateSavedObject(indexPattern, saveAttempts, ignoreErrors)](./kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md) | | Save existing index pattern. Will attempt to merge differences if there are conflicts | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md new file mode 100644 index 0000000000000..6b81447eca9ed --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [refreshFields](./kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md) + +## IndexPatternsService.refreshFields property + +Refresh field list for a given index pattern + +Signature: + +```typescript +refreshFields: (indexPattern: IndexPattern) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md new file mode 100644 index 0000000000000..92ac4e556ae29 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [savedObjectToSpec](./kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md) + +## IndexPatternsService.savedObjectToSpec property + +Converts index pattern saved object to index pattern spec + +Signature: + +```typescript +savedObjectToSpec: (savedObject: SavedObject) => IndexPatternSpec; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md new file mode 100644 index 0000000000000..708d645a79f1a --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [setDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md) + +## IndexPatternsService.setDefault property + +Optionally set default index pattern, unless force = true + +Signature: + +```typescript +setDefault: (id: string, force?: boolean) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md new file mode 100644 index 0000000000000..17f261aebdc65 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [updateSavedObject](./kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md) + +## IndexPatternsService.updateSavedObject() method + +Save existing index pattern. Will attempt to merge differences if there are conflicts + +Signature: + +```typescript +updateSavedObject(indexPattern: IndexPattern, saveAttempts?: number, ignoreErrors?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPattern | IndexPattern | | +| saveAttempts | number | | +| ignoreErrors | boolean | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md new file mode 100644 index 0000000000000..d408f00e33c9e --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) + +## IndexPatternsServiceProvider class + +Signature: + +```typescript +export declare class IndexPatternsServiceProvider implements Plugin +``` + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [setup(core, { expressions })](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md) | | | +| [start(core, { fieldFormats, logger })](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md) | | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md similarity index 67% rename from docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md rename to docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md index 6cac0a806d2ec..b5047d34efac1 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [setup](./kibana-plugin-plugins-data-server.indexpatternsservice.setup.md) +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) > [setup](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.setup.md) -## IndexPatternsService.setup() method +## IndexPatternsServiceProvider.setup() method Signature: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md similarity index 75% rename from docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md rename to docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md index 6528b1c213cca..98f9310c6d98c 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [start](./kibana-plugin-plugins-data-server.indexpatternsservice.start.md) +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) > [start](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md) -## IndexPatternsService.start() method +## IndexPatternsServiceProvider.start() method Signature: 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 4739de481e020..491babcdfdecf 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 @@ -12,6 +12,7 @@ | [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) | | | [IndexPatternsFetcher](./kibana-plugin-plugins-data-server.indexpatternsfetcher.md) | | | [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) | | +| [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.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.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index 9dc38f96df4be..f479ffd52e9b8 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -12,7 +12,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -31,7 +31,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }` diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index b57791db2b9fa..464cc2b1f54d1 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -146,6 +146,8 @@ export { UI_SETTINGS, IndexPattern, IndexPatternLoadExpressionFunctionDefinition, + IndexPatternsService, + IndexPatternsService as IndexPatternsCommonService, } from '../common'; /** @@ -306,4 +308,4 @@ export const config: PluginConfigDescriptor = { schema: configSchema, }; -export type { IndexPatternsServiceProvider as IndexPatternsService } from './index_patterns'; +export type { IndexPatternsServiceProvider } from './index_patterns'; diff --git a/src/plugins/data/server/index_patterns/index_patterns_service.ts b/src/plugins/data/server/index_patterns/index_patterns_service.ts index 1888feb93ec0d..5d703021b94da 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_service.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_service.ts @@ -19,7 +19,7 @@ import { DataPluginStartDependencies, DataPluginStart } from '../plugin'; import { registerRoutes } from './routes'; import { indexPatternSavedObjectType } from '../saved_objects'; import { capabilitiesProvider } from './capabilities_provider'; -import { IndexPatternsService as IndexPatternsCommonService } from '../../common/index_patterns'; +import { IndexPatternsCommonService } from '../'; import { FieldFormatsStart } from '../field_formats'; import { getIndexPatternLoad } from './expressions'; import { UiSettingsServerToCommon } from './ui_settings_wrapper'; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 23aaab36e7905..c33bd15589780 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -889,11 +889,54 @@ export class IndexPatternsFetcher { validatePatternListActive(patternList: string[]): Promise; } +// Warning: (ae-missing-release-tag) "IndexPatternsService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +class IndexPatternsService { + // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceDeps" needs to be exported by the entry point index.d.ts + constructor({ uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, }: IndexPatternsServiceDeps); + clearCache: (id?: string | undefined) => void; + create(spec: IndexPatternSpec, skipFetchFields?: boolean): Promise; + createAndSave(spec: IndexPatternSpec, override?: boolean, skipFetchFields?: boolean): Promise; + createSavedObject(indexPattern: IndexPattern, override?: boolean): Promise; + delete(indexPatternId: string): Promise<{}>; + // Warning: (ae-forgotten-export) The symbol "EnsureDefaultIndexPattern" needs to be exported by the entry point index.d.ts + // + // (undocumented) + ensureDefaultIndexPattern: EnsureDefaultIndexPattern; + // Warning: (ae-forgotten-export) The symbol "FieldAttrs" needs to be exported by the entry point index.d.ts + fieldArrayToMap: (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record; + find: (search: string, size?: number) => Promise; + get: (id: string) => Promise; + // Warning: (ae-forgotten-export) The symbol "IndexPatternSavedObjectAttrs" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getCache: () => Promise[] | null | undefined>; + getDefault: () => Promise; + getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; + // Warning: (ae-forgotten-export) The symbol "GetFieldsOptions" needs to be exported by the entry point index.d.ts + getFieldsForWildcard: (options: GetFieldsOptions) => Promise; + getIds: (refresh?: boolean) => Promise; + getIdsWithTitle: (refresh?: boolean) => Promise>; + getTitles: (refresh?: boolean) => Promise; + refreshFields: (indexPattern: IndexPattern) => Promise; + savedObjectToSpec: (savedObject: SavedObject_2) => IndexPatternSpec; + setDefault: (id: string, force?: boolean) => Promise; + updateSavedObject(indexPattern: IndexPattern, saveAttempts?: number, ignoreErrors?: boolean): Promise; +} + +export { IndexPatternsService as IndexPatternsCommonService } + +export { IndexPatternsService } + // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStart" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "IndexPatternsServiceProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class IndexPatternsService implements Plugin_3 { +export class IndexPatternsServiceProvider implements Plugin_3 { // Warning: (ae-forgotten-export) The symbol "DataPluginStartDependencies" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceSetupDeps" needs to be exported by the entry point index.d.ts // @@ -903,7 +946,7 @@ export class IndexPatternsService implements Plugin_3 Promise; + indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract_2, elasticsearchClient: ElasticsearchClient_2) => Promise; }; } @@ -1139,7 +1182,7 @@ export class Plugin implements Plugin_2 Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -1418,21 +1461,20 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:100:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:126:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:126:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:239:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:239:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:239:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:239:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:241:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:242:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:251:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:252:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:253:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:265:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:266:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index_patterns/index_patterns_service.ts:59:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:241:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:241:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:241:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:241:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:243:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:244:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:253:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:254:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:255:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:259:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:260:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:268:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:79: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:114:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts diff --git a/x-pack/plugins/maps/server/kibana_server_services.ts b/x-pack/plugins/maps/server/kibana_server_services.ts index 97c17dbe3b33c..6b59b460ad2c9 100644 --- a/x-pack/plugins/maps/server/kibana_server_services.ts +++ b/x-pack/plugins/maps/server/kibana_server_services.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { ISavedObjectsRepository } from 'kibana/server'; +import { ElasticsearchClient, ISavedObjectsRepository } from 'kibana/server'; +import { SavedObjectsClient } from '../../../../src/core/server'; +import { IndexPatternsCommonService } from '../../../../src/plugins/data/server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IndexPatternsServiceStart } from '../../../../src/plugins/data/server/index_patterns'; let internalRepository: ISavedObjectsRepository; export const setInternalRepository = ( @@ -14,3 +18,15 @@ export const setInternalRepository = ( internalRepository = createInternalRepository(); }; export const getInternalRepository = () => internalRepository; + +let indexPatternsService: IndexPatternsCommonService; +export const setIndexPatternsService = async ( + indexPatternsServiceFactory: IndexPatternsServiceStart['indexPatternsServiceFactory'], + elasticsearchClient: ElasticsearchClient +) => { + indexPatternsService = await indexPatternsServiceFactory( + new SavedObjectsClient(getInternalRepository()), + elasticsearchClient + ); +}; +export const getIndexPatternsService = () => indexPatternsService; diff --git a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.test.js b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.test.js index 7243dd84d6a85..8725e672ec368 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.test.js +++ b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.test.js @@ -6,13 +6,70 @@ */ import mapSavedObjects from './test_resources/sample_map_saved_objects.json'; -import indexPatternSavedObjects from './test_resources/sample_index_pattern_saved_objects'; import { buildMapsIndexPatternsTelemetry, buildMapsSavedObjectsTelemetry, getLayerLists, } from './maps_telemetry'; +jest.mock('../kibana_server_services', () => { + // Mocked for geo shape agg detection + const testAggIndexPatternId = '4a7f6010-0aed-11ea-9dd2-95afd7ad44d4'; + const testAggIndexPattern = { + id: testAggIndexPatternId, + fields: [ + { + name: 'geometry', + esTypes: ['geo_shape'], + }, + ], + }; + const testIndexPatterns = { + 1: { + id: '1', + fields: [ + { + name: 'one', + esTypes: ['geo_point'], + }, + ], + }, + 2: { + id: '2', + fields: [ + { + name: 'two', + esTypes: ['geo_point'], + }, + ], + }, + 3: { + id: '3', + fields: [ + { + name: 'three', + esTypes: ['geo_shape'], + }, + ], + }, + }; + return { + getIndexPatternsService() { + return { + async get(x) { + return x === testAggIndexPatternId ? testAggIndexPattern : testIndexPatterns[x]; + }, + async getIds() { + return Object.values(testIndexPatterns).map((x) => x.id); + }, + async getFieldsForIndexPattern(x) { + return x.fields; + }, + }; + }, + }; +}); + describe('buildMapsSavedObjectsTelemetry', () => { test('returns zeroed telemetry data when there are no saved objects', async () => { const result = buildMapsSavedObjectsTelemetry([]); @@ -88,7 +145,7 @@ describe('buildMapsSavedObjectsTelemetry', () => { test('returns expected telemetry data from index patterns', async () => { const layerLists = getLayerLists(mapSavedObjects); - const result = buildMapsIndexPatternsTelemetry(indexPatternSavedObjects, layerLists); + const result = await buildMapsIndexPatternsTelemetry(layerLists); expect(result).toMatchObject({ indexPatternsWithGeoFieldCount: 3, diff --git a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts index 5b098af760e65..0387d96046cb1 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts @@ -7,7 +7,7 @@ import _ from 'lodash'; import { SavedObject } from 'kibana/server'; -import { IFieldType, IndexPatternAttributes } from 'src/plugins/data/public'; +import { IFieldType } from 'src/plugins/data/public'; import { ES_GEO_FIELD_TYPE, LAYER_TYPE, @@ -22,7 +22,7 @@ import { LayerDescriptor, } from '../../common/descriptor_types'; import { MapSavedObject, MapSavedObjectAttributes } from '../../common/map_saved_object_type'; -import { getInternalRepository } from '../kibana_server_services'; +import { getIndexPatternsService, getInternalRepository } from '../kibana_server_services'; import { MapsConfigType } from '../../config'; interface Settings { @@ -94,37 +94,6 @@ function getUniqueLayerCounts(layerCountsList: ILayerTypeCount[], mapsCount: num }, {}); } -function getIndexPatternsWithGeoFieldCount( - indexPatterns: Array> -) { - const fieldLists = indexPatterns.map((indexPattern) => - indexPattern.attributes && indexPattern.attributes.fields - ? JSON.parse(indexPattern.attributes.fields) - : [] - ); - - const fieldListsWithGeoFields = fieldLists.filter((fields) => - fields.some( - (field: IFieldType) => - field.type === ES_GEO_FIELD_TYPE.GEO_POINT || field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE - ) - ); - - const fieldListsWithGeoPointFields = fieldLists.filter((fields) => - fields.some((field: IFieldType) => field.type === ES_GEO_FIELD_TYPE.GEO_POINT) - ); - - const fieldListsWithGeoShapeFields = fieldLists.filter((fields) => - fields.some((field: IFieldType) => field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE) - ); - - return { - indexPatternsWithGeoFieldCount: fieldListsWithGeoFields.length, - indexPatternsWithGeoPointFieldCount: fieldListsWithGeoPointFields.length, - indexPatternsWithGeoShapeFieldCount: fieldListsWithGeoShapeFields.length, - }; -} - function getEMSLayerCount(layerLists: LayerDescriptor[][]): ILayerTypeCount[] { return layerLists.map((layerList: LayerDescriptor[]) => { const emsLayers = layerList.filter((layer: LayerDescriptor) => { @@ -143,41 +112,25 @@ function getEMSLayerCount(layerLists: LayerDescriptor[][]): ILayerTypeCount[] { }) as ILayerTypeCount[]; } -function isFieldGeoShape( - indexPatterns: Array>, +async function isFieldGeoShape( indexPatternId: string, geoField: string | undefined -): boolean { - if (!geoField) { +): Promise { + if (!geoField || !indexPatternId) { return false; } - - const matchIndexPattern = indexPatterns.find( - (indexPattern: SavedObject) => { - return indexPattern.id === indexPatternId; - } - ); - - if (!matchIndexPattern) { + const indexPatternsService = await getIndexPatternsService(); + const indexPattern = await indexPatternsService.get(indexPatternId); + if (!indexPattern) { return false; } - - const fieldList: IFieldType[] = - matchIndexPattern.attributes && matchIndexPattern.attributes.fields - ? JSON.parse(matchIndexPattern.attributes.fields) - : []; - - const matchField = fieldList.find((field: IFieldType) => { - return field.name === geoField; - }); - - return !!matchField && matchField.type === ES_GEO_FIELD_TYPE.GEO_SHAPE; + const fieldsForIndexPattern = await indexPatternsService.getFieldsForIndexPattern(indexPattern); + return fieldsForIndexPattern.some( + (fieldDescriptor: IFieldType) => fieldDescriptor.name && fieldDescriptor.name === geoField! + ); } -function isGeoShapeAggLayer( - indexPatterns: Array>, - layer: LayerDescriptor -): boolean { +async function isGeoShapeAggLayer(layer: LayerDescriptor): Promise { if (layer.sourceDescriptor === null) { return false; } @@ -192,8 +145,7 @@ function isGeoShapeAggLayer( const sourceDescriptor = layer.sourceDescriptor; if (sourceDescriptor.type === SOURCE_TYPES.ES_GEO_GRID) { - return isFieldGeoShape( - indexPatterns, + return await isFieldGeoShape( (sourceDescriptor as ESGeoGridSourceDescriptor).indexPatternId, (sourceDescriptor as ESGeoGridSourceDescriptor).geoField ); @@ -201,8 +153,7 @@ function isGeoShapeAggLayer( sourceDescriptor.type === SOURCE_TYPES.ES_SEARCH && (sourceDescriptor as ESSearchSourceDescriptor).scalingType === SCALING_TYPES.CLUSTERS ) { - return isFieldGeoShape( - indexPatterns, + return await isFieldGeoShape( (sourceDescriptor as ESSearchSourceDescriptor).indexPatternId, (sourceDescriptor as ESSearchSourceDescriptor).geoField ); @@ -211,17 +162,15 @@ function isGeoShapeAggLayer( } } -function getGeoShapeAggCount( - layerLists: LayerDescriptor[][], - indexPatterns: Array> -): number { - const countsPerMap: number[] = layerLists.map((layerList: LayerDescriptor[]) => { - const geoShapeAggLayers = layerList.filter((layerDescriptor) => { - return isGeoShapeAggLayer(indexPatterns, layerDescriptor); - }); - return geoShapeAggLayers.length; - }); - +async function getGeoShapeAggCount(layerLists: LayerDescriptor[][]): Promise { + const countsPerMap: number[] = await Promise.all( + layerLists.map(async (layerList: LayerDescriptor[]) => { + const boolIsAggLayerArr = await Promise.all( + layerList.map(async (layerDescriptor) => await isGeoShapeAggLayer(layerDescriptor)) + ); + return boolIsAggLayerArr.filter((x) => x).length; + }) + ); return _.sum(countsPerMap); } @@ -235,30 +184,56 @@ export function getLayerLists(mapSavedObjects: MapSavedObject[]): LayerDescripto }); } -export function buildMapsIndexPatternsTelemetry( - indexPatternSavedObjects: Array>, - layerLists: LayerDescriptor[][] -): GeoIndexPatternsUsage { - const { - indexPatternsWithGeoFieldCount, - indexPatternsWithGeoPointFieldCount, - indexPatternsWithGeoShapeFieldCount, - } = getIndexPatternsWithGeoFieldCount(indexPatternSavedObjects); +async function filterIndexPatternsByField(fields: string[]) { + const indexPatternsService = await getIndexPatternsService(); + const indexPatternIds = await indexPatternsService.getIds(true); + let numIndexPatternsContainingField = 0; + await Promise.all( + indexPatternIds.map(async (indexPatternId: string) => { + const indexPattern = await indexPatternsService.get(indexPatternId); + const fieldsForIndexPattern = await indexPatternsService.getFieldsForIndexPattern( + indexPattern + ); + const containsField = fields.some((field: string) => + fieldsForIndexPattern.some( + (fieldDescriptor: IFieldType) => + fieldDescriptor.esTypes && fieldDescriptor.esTypes.includes(field) + ) + ); + if (containsField) { + numIndexPatternsContainingField++; + } + }) + ); + return numIndexPatternsContainingField; +} +export async function buildMapsIndexPatternsTelemetry( + layerLists: LayerDescriptor[][] +): Promise { + const indexPatternsWithGeoField = await filterIndexPatternsByField([ + ES_GEO_FIELD_TYPE.GEO_POINT, + ES_GEO_FIELD_TYPE.GEO_SHAPE, + ]); + const indexPatternsWithGeoPointField = await filterIndexPatternsByField([ + ES_GEO_FIELD_TYPE.GEO_POINT, + ]); + const indexPatternsWithGeoShapeField = await filterIndexPatternsByField([ + ES_GEO_FIELD_TYPE.GEO_SHAPE, + ]); // Tracks whether user uses Gold+ only functionality - const geoShapeAggLayersCount = getGeoShapeAggCount(layerLists, indexPatternSavedObjects); + const geoShapeAggLayersCount = await getGeoShapeAggCount(layerLists); return { - indexPatternsWithGeoFieldCount, - indexPatternsWithGeoPointFieldCount, - indexPatternsWithGeoShapeFieldCount, + indexPatternsWithGeoFieldCount: indexPatternsWithGeoField, + indexPatternsWithGeoPointFieldCount: indexPatternsWithGeoPointField, + indexPatternsWithGeoShapeFieldCount: indexPatternsWithGeoShapeField, geoShapeAggLayersCount, }; } export function buildMapsSavedObjectsTelemetry(layerLists: LayerDescriptor[][]): LayersStatsUsage { const mapsCount = layerLists.length; - const dataSourcesCount = layerLists.map((layerList: LayerDescriptor[]) => { // todo: not every source-descriptor has an id // @ts-ignore @@ -340,16 +315,7 @@ export async function getMapsTelemetry(config: MapsConfigType): Promise( - 'index-pattern', - (savedObjects) => - _.mergeWith( - indexPatternsTelemetry, - buildMapsIndexPatternsTelemetry(savedObjects, layerLists), - (prevVal, currVal) => prevVal || 0 + currVal || 0 // Additive merge - ) - ); + const indexPatternsTelemetry = await buildMapsIndexPatternsTelemetry(layerLists); return { settings: { diff --git a/x-pack/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json b/x-pack/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json deleted file mode 100644 index 0b36d5ff84016..0000000000000 --- a/x-pack/plugins/maps/server/maps_telemetry/test_resources/sample_index_pattern_saved_objects.json +++ /dev/null @@ -1,59 +0,0 @@ -[ - { - "attributes": { - "fields": "[{\"name\":\"geometry\",\"type\":\"geo_shape\",\"esTypes\":[\"geo_shape\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false}]", - "timeFieldName": "ORIG_DATE", - "title": "indexpattern-with-geoshape" - }, - "id": "4a7f6010-0aed-11ea-9dd2-95afd7ad44d4", - "migrationVersion": { - "index-pattern": "7.6.0" - }, - "references": [], - "type": "index-pattern", - "updated_at": "2019-11-19T16:54:46.405Z", - "version": "Wzg0LDFd" - }, - { - "attributes": { - "fields": "[{\"name\":\"geometry\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", - "title": "indexpattern-with-geopoint" - }, - "id": "55d572f0-0b07-11ea-9dd2-95afd7ad44d4", - "migrationVersion": { - "index-pattern": "7.6.0" - }, - "references": [], - "type": "index-pattern", - "updated_at": "2019-11-19T20:05:37.607Z", - "version": "WzExMSwxXQ==" - }, - { - "attributes": { - "fields": "[{\"name\":\"geometry\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", - "title": "indexpattern-with-geopoint2" - }, - "id": "55d572f0-0b07-11ea-9dd2-95afd7ad44d4", - "migrationVersion": { - "index-pattern": "7.6.0" - }, - "references": [], - "type": "index-pattern", - "updated_at": "2019-11-19T20:05:37.607Z", - "version": "WzExMSwxXQ==" - }, - { - "attributes": { - "fields": "[{\"name\":\"assessment_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"date_exterior_condition\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"recording_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"sale_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", - "title": "indexpattern-without-geo" - }, - "id": "55d572f0-0b07-11ea-9dd2-95afd7ad44d4", - "migrationVersion": { - "index-pattern": "7.6.0" - }, - "references": [], - "type": "index-pattern", - "updated_at": "2019-11-19T20:05:37.607Z", - "version": "WzExMSwxXQ==" - } -] diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index cb22a98b70aa8..4118074841aef 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -20,7 +20,7 @@ import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, getExistingMapPath } from '../ import { mapSavedObjects, mapsTelemetrySavedObjects } from './saved_objects'; import { MapsXPackConfig } from '../config'; // @ts-ignore -import { setInternalRepository } from './kibana_server_services'; +import { setIndexPatternsService, setInternalRepository } from './kibana_server_services'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { emsBoundariesSpecProvider } from './tutorials/ems'; // @ts-ignore @@ -30,6 +30,7 @@ import { LicensingPluginSetup } from '../../licensing/server'; import { HomeServerPluginSetup } from '../../../../src/plugins/home/server'; import { MapsLegacyPluginSetup } from '../../../../src/plugins/maps_legacy/server'; import { EMSSettings } from '../common/ems_settings'; +import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; interface SetupDeps { features: FeaturesPluginSetupContract; @@ -39,6 +40,10 @@ interface SetupDeps { mapsLegacy: MapsLegacyPluginSetup; } +export interface StartDeps { + data: DataPluginStart; +} + export class MapsPlugin implements Plugin { readonly _initializerContext: PluginInitializerContext; private readonly _logger: Logger; @@ -208,7 +213,11 @@ export class MapsPlugin implements Plugin { } // @ts-ignore - start(core: CoreStart) { + start(core: CoreStart, plugins: StartDeps) { setInternalRepository(core.savedObjects.createInternalRepository); + setIndexPatternsService( + plugins.data.indexPatterns.indexPatternsServiceFactory, + core.elasticsearch.client.asInternalUser + ); } } From a108469ec78a6df4d7628eb94f492b583e7014e6 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Fri, 19 Feb 2021 10:50:35 -0500 Subject: [PATCH 09/32] Allowing deletion of collections (#91926) --- .../server/routes/api/cases/delete_cases.ts | 44 ------------ .../basic/tests/cases/delete_cases.ts | 71 ++++++++++++++++++- 2 files changed, 70 insertions(+), 45 deletions(-) diff --git a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts index 263b814df4146..497e33d7feb30 100644 --- a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts @@ -8,43 +8,12 @@ import { schema } from '@kbn/config-schema'; import { SavedObjectsClientContract } from 'src/core/server'; -import { CaseType } from '../../../../common/api'; import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; import { CASES_URL } from '../../../../common/constants'; import { CaseServiceSetup } from '../../../services'; -async function unremovableCases({ - caseService, - client, - ids, - force, -}: { - caseService: CaseServiceSetup; - client: SavedObjectsClientContract; - ids: string[]; - force: boolean | undefined; -}): Promise { - // if the force flag was included then we can skip checking whether the cases are collections and go ahead - // and delete them - if (force) { - return []; - } - - const cases = await caseService.getCases({ caseIds: ids, client }); - const parentCases = cases.saved_objects.filter( - /** - * getCases will return an array of saved_objects and some can be successful cases where as others - * might have failed to find the ID. If it fails to find it, it will set the error field but not - * the attributes so check that we didn't receive an error. - */ - (caseObj) => !caseObj.error && caseObj.attributes.type === CaseType.collection - ); - - return parentCases.map((parentCase) => parentCase.id); -} - async function deleteSubCases({ caseService, client, @@ -84,25 +53,12 @@ export function initDeleteCasesApi({ caseService, router, userActionService }: R validate: { query: schema.object({ ids: schema.arrayOf(schema.string()), - force: schema.maybe(schema.boolean()), }), }, }, async (context, request, response) => { try { const client = context.core.savedObjects.client; - const unremovable = await unremovableCases({ - caseService, - client, - ids: request.query.ids, - force: request.query.force, - }); - - if (unremovable.length > 0) { - return response.badRequest({ - body: `Case IDs: [${unremovable.join(' ,')}] are not removable`, - }); - } await Promise.all( request.query.ids.map((id) => caseService.deleteCase({ diff --git a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts index 1ea4712e260f0..8edc3b0d08113 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts @@ -10,7 +10,17 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../plugins/case/common/constants'; import { postCaseReq, postCommentUserReq } from '../../../common/lib/mock'; -import { deleteCases, deleteCasesUserActions, deleteComments } from '../../../common/lib/utils'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + deleteCases, + deleteCasesUserActions, + deleteComments, +} from '../../../common/lib/utils'; +import { getSubCaseDetailsUrl } from '../../../../../plugins/case/common/api/helpers'; +import { CollectionWithSubCaseResponse } from '../../../../../plugins/case/common/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -79,5 +89,64 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(404); }); + + describe('sub cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should delete the sub cases when deleting a collection', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(caseInfo.subCase?.id).to.not.eql(undefined); + + const { body } = await supertest + .delete(`${CASES_URL}?ids=["${caseInfo.id}"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + expect(body).to.eql({}); + await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id)) + .send() + .expect(404); + }); + + it(`should delete a sub case's comments when that case gets deleted`, async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(caseInfo.subCase?.id).to.not.eql(undefined); + + // there should be two comments on the sub case now + const { + body: patchedCaseWithSubCase, + }: { body: CollectionWithSubCaseResponse } = await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments`) + .set('kbn-xsrf', 'true') + .query({ subCaseID: caseInfo.subCase!.id }) + .send(postCommentUserReq) + .expect(200); + + const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${ + patchedCaseWithSubCase.subCase!.comments![1].id + }`; + // make sure we can get the second comment + await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); + + await supertest + .delete(`${CASES_URL}?ids=["${caseInfo.id}"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(404); + }); + }); }); }; From d6984ca160fe40b615e2fd813d8318451b09d9a5 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Fri, 19 Feb 2021 10:56:52 -0500 Subject: [PATCH 10/32] [Maps] upgrade mapbox-gl to v1.13.1 (#91564) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a65c12fce4699..67263f53f28a2 100644 --- a/package.json +++ b/package.json @@ -724,7 +724,7 @@ "loader-utils": "^1.2.3", "log-symbols": "^2.2.0", "lz-string": "^1.4.4", - "mapbox-gl": "^1.12.0", + "mapbox-gl": "1.13.1", "mapbox-gl-draw-rectangle-mode": "^1.0.4", "marge": "^1.0.1", "memoize-one": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index 4738925e44dfb..8a8147bd25aef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20250,10 +20250,10 @@ mapbox-gl-draw-rectangle-mode@^1.0.4: resolved "https://registry.yarnpkg.com/mapbox-gl-draw-rectangle-mode/-/mapbox-gl-draw-rectangle-mode-1.0.4.tgz#42987d68872a5fb5cc5d76d3375ee20cd8bab8f7" integrity sha512-BdF6nwEK2p8n9LQoMPzBO8LhddW1fe+d5vK8HQIei+4VcRnUbKNsEj7Z15FsJxCHzsc2BQKXbESx5GaE8x0imQ== -mapbox-gl@^1.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-1.12.0.tgz#7d1c73b1153d7ee219d30d80728d7df079bc7c05" - integrity sha512-B3URR4qY9R/Bx+DKqP8qmGCai8IOZYMSZF7ZSvcCZaYTaOYhQQi8ErTEDZtFMOR0ZPj7HFWOkkhl5SqvDfpJpA== +mapbox-gl@1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-1.13.1.tgz#322efe75ab4c764fc4c776da1506aad58d5a5b9d" + integrity sha512-GSyubcoSF5MyaP8z+DasLu5v7KmDK2pp4S5+VQ5WdVQUOaAqQY4jwl4JpcdNho3uWm2bIKs7x1l7q3ynGmW60g== dependencies: "@mapbox/geojson-rewind" "^0.5.0" "@mapbox/geojson-types" "^1.0.2" From 99a60caf81ea043b1e0e67d678a2ea03fd58b060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 19 Feb 2021 11:15:40 -0500 Subject: [PATCH 11/32] [APM] Bug: Service overview - Sparkline loading state icons has changed (#91884) * adjusting icon size * Updating color --- .../components/shared/charts/spark_plot/index.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx index 36c499f9e5ee4..a89d36f708990 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx @@ -68,7 +68,16 @@ export function SparkPlot({ {!series || series.every((point) => point.y === null) ? ( - +
+ +
) : ( Date: Fri, 19 Feb 2021 16:41:00 +0000 Subject: [PATCH 12/32] [Discover] Always show unmapped fields (#91735) * [Discover] Always show unmapped fields * Updating the functional test * Remove unmapped switch toggle * Some more code cleanup Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/angular/discover.js | 11 +-- .../sidebar/discover_field_search.test.tsx | 18 ----- .../sidebar/discover_field_search.tsx | 77 +++---------------- .../components/sidebar/discover_sidebar.tsx | 2 - .../discover_sidebar_responsive.test.tsx | 2 - .../sidebar/discover_sidebar_responsive.tsx | 13 +--- .../public/application/components/types.ts | 13 +--- .../embeddable/search_embeddable.ts | 7 +- .../public/saved_searches/_saved_search.ts | 3 - .../discover/public/saved_searches/types.ts | 1 - .../discover/server/saved_objects/search.ts | 1 - .../saved_objects/search_migrations.test.ts | 37 --------- .../server/saved_objects/search_migrations.ts | 19 ----- .../_indexpattern_with_unmapped_fields.ts | 29 +------ 14 files changed, 17 insertions(+), 216 deletions(-) diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 78ad40e48fd96..88747cf9e84d8 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -675,19 +675,10 @@ function discoverController($route, $scope, Promise) { history.push('/'); }; - const showUnmappedFieldsDefaultValue = $scope.useNewFieldsApi && !!$scope.opts.savedSearch.pre712; - let showUnmappedFields = showUnmappedFieldsDefaultValue; - - const onChangeUnmappedFields = (value) => { - showUnmappedFields = value; - $scope.unmappedFieldsConfig.showUnmappedFields = value; - $scope.fetch(); - }; + const showUnmappedFields = $scope.useNewFieldsApi; $scope.unmappedFieldsConfig = { - showUnmappedFieldsDefaultValue, showUnmappedFields, - onChangeUnmappedFields, }; $scope.updateDataSource = () => { diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx index 797a6c9697c35..04562cbd26520 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx @@ -136,22 +136,4 @@ describe('DiscoverFieldSearch', () => { popover = component.find(EuiPopover); expect(popover.prop('isOpen')).toBe(false); }); - - test('unmapped fields', () => { - const onChangeUnmappedFields = jest.fn(); - const componentProps = { - ...defaultProps, - showUnmappedFields: true, - useNewFieldsApi: false, - onChangeUnmappedFields, - }; - const component = mountComponent(componentProps); - const btn = findTestSubject(component, 'toggleFieldFilterButton'); - btn.simulate('click'); - const unmappedFieldsSwitch = findTestSubject(component, 'unmappedFieldsSwitch'); - act(() => { - unmappedFieldsSwitch.simulate('click'); - }); - expect(onChangeUnmappedFields).toHaveBeenCalledWith(false); - }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx index 8fb90bfea3a95..1e99959d77134 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx @@ -27,8 +27,6 @@ import { EuiOutsideClickDetector, EuiFilterButton, EuiSpacer, - EuiIcon, - EuiToolTip, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -37,7 +35,6 @@ export interface State { aggregatable: string; type: string; missing: boolean; - unmappedFields: boolean; [index: string]: string | boolean; } @@ -61,31 +58,13 @@ export interface Props { * use new fields api */ useNewFieldsApi?: boolean; - - /** - * callback funtion to change the value of unmapped fields switch - * @param value new value to set - */ - onChangeUnmappedFields?: (value: boolean) => void; - - /** - * should unmapped fields switch be rendered - */ - showUnmappedFields?: boolean; } /** * Component is Discover's side bar to search of available fields * Additionally there's a button displayed that allows the user to show/hide more filter fields */ -export function DiscoverFieldSearch({ - onChange, - value, - types, - useNewFieldsApi, - showUnmappedFields, - onChangeUnmappedFields, -}: Props) { +export function DiscoverFieldSearch({ onChange, value, types, useNewFieldsApi }: Props) { const searchPlaceholder = i18n.translate('discover.fieldChooser.searchPlaceHolder', { defaultMessage: 'Search field names', }); @@ -111,7 +90,6 @@ export function DiscoverFieldSearch({ aggregatable: 'any', type: 'any', missing: true, - unmappedFields: !!showUnmappedFields, }); if (typeof value !== 'string') { @@ -181,14 +159,6 @@ export function DiscoverFieldSearch({ handleValueChange('missing', missingValue); }; - const handleUnmappedFieldsChange = (e: EuiSwitchEvent) => { - const unmappedFieldsValue = e.target.checked; - handleValueChange('unmappedFields', unmappedFieldsValue); - if (onChangeUnmappedFields) { - onChangeUnmappedFields(unmappedFieldsValue); - } - }; - const buttonContent = ( { - if (!showUnmappedFields && useNewFieldsApi) { + if (useNewFieldsApi) { return null; } return ( - {showUnmappedFields ? ( - - - - - - - - - - - ) : null} - {useNewFieldsApi ? null : ( - - )} + ); }; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index f0303553dfac0..c0a192550e6c4 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -205,8 +205,6 @@ export function DiscoverSidebar({ value={fieldFilter.name} types={fieldTypes} useNewFieldsApi={useNewFieldsApi} - onChangeUnmappedFields={unmappedFieldsConfig?.onChangeUnmappedFields} - showUnmappedFields={unmappedFieldsConfig?.showUnmappedFieldsDefaultValue} />
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx index 7b12ab5f9bcd9..79e8caabd4930 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx @@ -137,9 +137,7 @@ describe('discover responsive sidebar', function () { }); it('renders sidebar with unmapped fields config', function () { const unmappedFieldsConfig = { - onChangeUnmappedFields: jest.fn(), showUnmappedFields: false, - showUnmappedFieldsDefaultValue: false, }; const componentProps = { ...props, unmappedFieldsConfig }; const component = mountWithIntl(); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx index b689db1296922..f0e7c71f9c970 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx @@ -113,24 +113,13 @@ export interface DiscoverSidebarResponsiveProps { useNewFieldsApi?: boolean; /** - * an object containing properties for proper handling of unmapped fields in the UI + * an object containing properties for proper handling of unmapped fields */ unmappedFieldsConfig?: { - /** - * callback function to change the value of `showUnmappedFields` flag - * @param value new value to set - */ - onChangeUnmappedFields: (value: boolean) => void; /** * determines whether to display unmapped fields - * configurable through the switch in the UI */ showUnmappedFields: boolean; - /** - * determines if we should display an option to toggle showUnmappedFields value in the first place - * this value is not configurable through the UI - */ - showUnmappedFieldsDefaultValue: boolean; }; } diff --git a/src/plugins/discover/public/application/components/types.ts b/src/plugins/discover/public/application/components/types.ts index e276795f9ed7f..e488f596cece8 100644 --- a/src/plugins/discover/public/application/components/types.ts +++ b/src/plugins/discover/public/application/components/types.ts @@ -159,23 +159,12 @@ export interface DiscoverProps { */ timeRange?: { from: string; to: string }; /** - * An object containing properties for proper handling of unmapped fields in the UI + * An object containing properties for unmapped fields behavior */ unmappedFieldsConfig?: { /** * determines whether to display unmapped fields - * configurable through the switch in the UI */ showUnmappedFields: boolean; - /** - * determines if we should display an option to toggle showUnmappedFields value in the first place - * this value is not configurable through the UI - */ - showUnmappedFieldsDefaultValue: boolean; - /** - * callback function to change the value of `showUnmappedFields` flag - * @param value new value to set - */ - onChangeUnmappedFields: (value: boolean) => void; }; } diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index 658734aa46cb0..2bafa23907502 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -291,7 +291,7 @@ export class SearchEmbeddable const useNewFieldsApi = !this.services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE, false); if (!this.searchScope) return; - const { searchSource, pre712 } = this.savedSearch; + const { searchSource } = this.savedSearch; // Abort any in-progress requests if (this.abortController) this.abortController.abort(); @@ -308,10 +308,7 @@ export class SearchEmbeddable ); if (useNewFieldsApi) { searchSource.removeField('fieldsFromSource'); - const fields: Record = { field: '*' }; - if (pre712) { - fields.include_unmapped = 'true'; - } + const fields: Record = { field: '*', include_unmapped: 'true' }; searchSource.setField('fields', [fields]); } else { searchSource.removeField('fields'); diff --git a/src/plugins/discover/public/saved_searches/_saved_search.ts b/src/plugins/discover/public/saved_searches/_saved_search.ts index a7b6ef49cacd2..320332ca4ace5 100644 --- a/src/plugins/discover/public/saved_searches/_saved_search.ts +++ b/src/plugins/discover/public/saved_searches/_saved_search.ts @@ -20,7 +20,6 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { grid: 'object', sort: 'keyword', version: 'integer', - pre712: 'boolean', }; // Order these fields to the top, the rest are alphabetical public static fieldOrder = ['title', 'description']; @@ -42,7 +41,6 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { grid: 'object', sort: 'keyword', version: 'integer', - pre712: 'boolean', }, searchSource: true, defaults: { @@ -52,7 +50,6 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { hits: 0, sort: [], version: 1, - pre712: false, }, }); this.showInRecentlyAccessed = true; diff --git a/src/plugins/discover/public/saved_searches/types.ts b/src/plugins/discover/public/saved_searches/types.ts index 4646744ee0ef3..b1c7b48d696b3 100644 --- a/src/plugins/discover/public/saved_searches/types.ts +++ b/src/plugins/discover/public/saved_searches/types.ts @@ -23,7 +23,6 @@ export interface SavedSearch { save: (saveOptions: SavedObjectSaveOpts) => Promise; lastSavedTitle?: string; copyOnSave?: boolean; - pre712?: boolean; hideChart?: boolean; } export interface SavedSearchLoader { diff --git a/src/plugins/discover/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts index de3a2197fe0ac..b66c06db3e120 100644 --- a/src/plugins/discover/server/saved_objects/search.ts +++ b/src/plugins/discover/server/saved_objects/search.ts @@ -45,7 +45,6 @@ export const searchSavedObjectType: SavedObjectsType = { title: { type: 'text' }, grid: { type: 'object', enabled: false }, version: { type: 'integer' }, - pre712: { type: 'boolean' }, }, }, migrations: searchMigrations as any, diff --git a/src/plugins/discover/server/saved_objects/search_migrations.test.ts b/src/plugins/discover/server/saved_objects/search_migrations.test.ts index f1dc228a9ac08..fb608c0b6f3e8 100644 --- a/src/plugins/discover/server/saved_objects/search_migrations.test.ts +++ b/src/plugins/discover/server/saved_objects/search_migrations.test.ts @@ -350,41 +350,4 @@ Object { testMigrateMatchAllQuery(migrationFn); }); }); - - describe('7.12.0', () => { - const migrationFn = searchMigrations['7.12.0']; - - describe('migrateExistingSavedSearch', () => { - it('should add a new flag to existing saved searches', () => { - const migratedDoc = migrationFn( - { - type: 'search', - attributes: { - kibanaSavedObjectMeta: {}, - }, - }, - savedObjectMigrationContext - ); - const migratedPre712Flag = migratedDoc.attributes.pre712; - - expect(migratedPre712Flag).toEqual(true); - }); - - it('should not modify a flag if it already exists', () => { - const migratedDoc = migrationFn( - { - type: 'search', - attributes: { - kibanaSavedObjectMeta: {}, - pre712: false, - }, - }, - savedObjectMigrationContext - ); - const migratedPre712Flag = migratedDoc.attributes.pre712; - - expect(migratedPre712Flag).toEqual(false); - }); - }); - }); }); diff --git a/src/plugins/discover/server/saved_objects/search_migrations.ts b/src/plugins/discover/server/saved_objects/search_migrations.ts index 72749bfd2e9cd..feaf91409797a 100644 --- a/src/plugins/discover/server/saved_objects/search_migrations.ts +++ b/src/plugins/discover/server/saved_objects/search_migrations.ts @@ -117,28 +117,9 @@ const migrateSearchSortToNestedArray: SavedObjectMigrationFn = (doc) = }; }; -const migrateExistingSavedSearch: SavedObjectMigrationFn = (doc) => { - if (!doc.attributes) { - return doc; - } - const pre712 = doc.attributes.pre712; - // pre712 already has a value - if (pre712 !== undefined) { - return doc; - } - return { - ...doc, - attributes: { - ...doc.attributes, - pre712: true, - }, - }; -}; - export const searchMigrations = { '6.7.2': flow(migrateMatchAllQuery), '7.0.0': flow(setNewReferences), '7.4.0': flow(migrateSearchSortToNestedArray), '7.9.3': flow(migrateMatchAllQuery), - '7.12.0': flow(migrateExistingSavedSearch), }; diff --git a/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts b/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts index 0990b3fa29f70..06933e828db7e 100644 --- a/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts +++ b/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts @@ -12,14 +12,11 @@ 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 log = getService('log'); const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); describe('index pattern with unmapped fields', () => { - const unmappedFieldsSwitchSelector = 'unmappedFieldsSwitch'; - before(async () => { await esArchiver.loadIfNeeded('unmapped_fields'); await kibanaServer.uiSettings.replace({ @@ -37,7 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.unload('unmapped_fields'); }); - it('unmapped fields do not exist on a new saved search', async () => { + it('unmapped fields exist on a new saved search', async () => { const expectedHitCount = '4'; await retry.try(async function () { expect(await PageObjects.discover.getHitCount()).to.be(expectedHitCount); @@ -46,13 +43,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // message is a mapped field expect(allFields.includes('message')).to.be(true); // sender is not a mapped field - expect(allFields.includes('sender')).to.be(false); - }); - - it('unmapped fields toggle does not exist on a new saved search', async () => { - await PageObjects.discover.openSidebarFieldFilter(); - await testSubjects.existOrFail('filterSelectionPanel'); - await testSubjects.missingOrFail('unmappedFieldsSwitch'); + expect(allFields.includes('sender')).to.be(true); }); it('unmapped fields exist on an existing saved search', async () => { @@ -66,21 +57,5 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(allFields.includes('sender')).to.be(true); expect(allFields.includes('receiver')).to.be(true); }); - - it('unmapped fields toggle exists on an existing saved search', async () => { - await PageObjects.discover.openSidebarFieldFilter(); - await testSubjects.existOrFail('filterSelectionPanel'); - await testSubjects.existOrFail(unmappedFieldsSwitchSelector); - expect(await testSubjects.isEuiSwitchChecked(unmappedFieldsSwitchSelector)).to.be(true); - }); - - it('switching unmapped fields toggle off hides unmapped fields', async () => { - await testSubjects.setEuiSwitch(unmappedFieldsSwitchSelector, 'uncheck'); - await PageObjects.discover.closeSidebarFieldFilter(); - const allFields = await PageObjects.discover.getAllFieldNames(); - expect(allFields.includes('message')).to.be(true); - expect(allFields.includes('sender')).to.be(false); - expect(allFields.includes('receiver')).to.be(false); - }); }); } From 10b1fddf356f2d871aee5bd195d76245370e1af0 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Fri, 19 Feb 2021 08:49:48 -0800 Subject: [PATCH 13/32] [Fleet] Handle long text in agent details page (#91776) * Fix #85521 * Set a minimum height for agent logs component #89831 * Truncate long integration names nicely #85404 --- .../agent_details/agent_details_integrations.tsx | 14 +++++++++----- .../agent_details/agent_details_overview.tsx | 4 +++- .../components/agent_logs/agent_logs.tsx | 9 +++++++-- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx index d8beabab67ef1..d71fb8be5f9cf 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx @@ -27,8 +27,7 @@ import { displayInputType, getLogsQueryByInputType } from './input_type_utils'; const StyledEuiAccordion = styled(EuiAccordion)` .ingest-integration-title-button { - padding: ${(props) => props.theme.eui.paddingSizes.m} - ${(props) => props.theme.eui.paddingSizes.m}; + padding: ${(props) => props.theme.eui.paddingSizes.m}; } &.euiAccordion-isOpen .ingest-integration-title-button { @@ -38,6 +37,10 @@ const StyledEuiAccordion = styled(EuiAccordion)` .euiTableRow:last-child .euiTableRowCell { border-bottom: none; } + + .euiIEFlexWrapFix { + min-width: 0; + } `; const CollapsablePanel: React.FC<{ id: string; title: React.ReactNode }> = ({ @@ -46,11 +49,11 @@ const CollapsablePanel: React.FC<{ id: string; title: React.ReactNode }> = ({ children, }) => { return ( - + {children} @@ -128,8 +131,9 @@ export const AgentDetailsIntegration: React.FunctionComponent<{ )} - + {title} - {description} + + {description} +
); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx index 423467702e05a..fafe389d07b82 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx @@ -185,8 +185,6 @@ export const AgentLogsUI: React.FunctionComponent = memo(({ agen [http.basePath, state.start, state.end, logStreamQuery] ); - const [logsPanelRef, { height: logPanelHeight }] = useMeasure(); - const agentVersion = agent.local_metadata?.elastic?.agent?.version; const isLogFeatureAvailable = useMemo(() => { if (!agentVersion) { @@ -199,6 +197,13 @@ export const AgentLogsUI: React.FunctionComponent = memo(({ agen return semverGte(agentVersionWithPrerelease, '7.11.0'); }, [agentVersion]); + // Set absolute height on logs component (needed to render correctly in Safari) + // based on available height, or 600px, whichever is greater + const [logsPanelRef, { height: measuredlogPanelHeight }] = useMeasure(); + const logPanelHeight = useMemo(() => Math.max(measuredlogPanelHeight, 600), [ + measuredlogPanelHeight, + ]); + if (!isLogFeatureAvailable) { return ( Date: Fri, 19 Feb 2021 10:28:50 -0700 Subject: [PATCH 14/32] Unskip Search Sessions Management UI test (#90110) * Unskip Search Sessions Management UI test * more logging * logging * ci test * skip reload test * add tm task to archives used by dependent tests * --wip-- [skip ci] * revert jest affecting changes * fix search sessions archive * add pagination test * test organize * log cleanup * fix async in tests * remove obsolete test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/search/session/index.ts | 2 + .../sessions_mgmt/components/table/table.tsx | 9 +- .../dashboard/async_search/data.json | 31 ++ .../dashboard/async_search/mappings.json | 90 ++++ .../data/search_sessions/data.json.gz | Bin 1976 -> 2650 bytes .../data/search_sessions/mappings.json | 422 +++++++++--------- .../search_sessions_management_page.ts | 7 +- .../config.ts | 2 +- .../services/search_sessions.ts | 4 +- .../apps/dashboard/async_search/index.ts | 2 +- .../tests/apps/discover/index.ts | 2 +- .../search_sessions/sessions_management.ts | 149 +++++-- .../sessions_management_permissions.ts | 10 +- 13 files changed, 456 insertions(+), 274 deletions(-) diff --git a/x-pack/plugins/data_enhanced/common/search/session/index.ts b/x-pack/plugins/data_enhanced/common/search/session/index.ts index e83137308be98..45b5c16bca957 100644 --- a/x-pack/plugins/data_enhanced/common/search/session/index.ts +++ b/x-pack/plugins/data_enhanced/common/search/session/index.ts @@ -7,3 +7,5 @@ export * from './status'; export * from './types'; + +export const SEARCH_SESSIONS_TABLE_ID = 'searchSessionsMgmtUiTable'; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx index 6139f3ef8a847..40ed0205d8dc9 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx @@ -9,11 +9,12 @@ import { EuiButton, EuiInMemoryTable, EuiSearchBarProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { CoreStart } from 'kibana/public'; import moment from 'moment'; -import React, { useCallback, useMemo, useRef, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import useInterval from 'react-use/lib/useInterval'; import { TableText } from '../'; import { IManagementSectionsPluginsSetup, SessionsConfigSchema } from '../..'; +import { SEARCH_SESSIONS_TABLE_ID } from '../../../../../common/search'; import { SearchSessionsMgmtAPI } from '../../lib/api'; import { getColumns } from '../../lib/get_columns'; import { UISession } from '../../types'; @@ -21,8 +22,6 @@ import { OnActionComplete } from '../actions'; import { getAppFilter } from './app_filter'; import { getStatusFilter } from './status_filter'; -const TABLE_ID = 'searchSessionsMgmtTable'; - interface Props { core: CoreStart; api: SearchSessionsMgmtAPI; @@ -107,8 +106,8 @@ export function SearchSessionsMgmtTable({ core, api, timezone, config, plugins, return ( {...props} - id={TABLE_ID} - data-test-subj={TABLE_ID} + id={SEARCH_SESSIONS_TABLE_ID} + data-test-subj={SEARCH_SESSIONS_TABLE_ID} rowProps={() => ({ 'data-test-subj': 'searchSessionsRow', })} diff --git a/x-pack/test/functional/es_archives/dashboard/async_search/data.json b/x-pack/test/functional/es_archives/dashboard/async_search/data.json index 486c73f711a6b..90c890d0f041d 100644 --- a/x-pack/test/functional/es_archives/dashboard/async_search/data.json +++ b/x-pack/test/functional/es_archives/dashboard/async_search/data.json @@ -243,3 +243,34 @@ } } +{ + "type": "doc", + "value": { + "id": "task:data_enhanced_search_sessions_monitor", + "index": ".kibana_task_manager_1", + "source": { + "references": [], + "task": { + "attempts": 0, + "ownerId": null, + "params": "{}", + "retryAt": "2020-11-30T15:43:39.626Z", + "runAt": "2020-11-30T15:43:08.277Z", + "scheduledAt": "2020-11-30T15:43:08.277Z", + "retryAt": null, + "schedule": { + "interval": "3s" + }, + "scope": [ + "testing" + ], + "startedAt": null, + "state": "{}", + "status": "idle", + "taskType": "search_sessions_monitor" + }, + "type": "task", + "updated_at": "2020-11-30T15:43:08.277Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/dashboard/async_search/mappings.json b/x-pack/test/functional/es_archives/dashboard/async_search/mappings.json index 210fade40c648..ee860fe973f60 100644 --- a/x-pack/test/functional/es_archives/dashboard/async_search/mappings.json +++ b/x-pack/test/functional/es_archives/dashboard/async_search/mappings.json @@ -242,3 +242,93 @@ } } } + +{ + "type": "index", + "value": { + "aliases": { + ".kibana_task_manager": { + } + }, + "index": ".kibana_task_manager_1", + "mappings": { + "dynamic": "strict", + "properties": { + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "task": { + "properties": { + "attempts": { + "type": "integer" + }, + "ownerId": { + "type": "keyword" + }, + "params": { + "type": "text" + }, + "retryAt": { + "type": "date" + }, + "runAt": { + "type": "date" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledAt": { + "type": "date" + }, + "scope": { + "type": "keyword" + }, + "startedAt": { + "type": "date" + }, + "state": { + "type": "text" + }, + "status": { + "type": "keyword" + }, + "taskType": { + "type": "keyword" + }, + "user": { + "type": "keyword" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/data/search_sessions/data.json.gz b/x-pack/test/functional/es_archives/data/search_sessions/data.json.gz index 51e8c09f19247f979a9f1823ab5786b99cf849e4..fff020036a8e3de67513dbdb85f1bf96c850605d 100644 GIT binary patch literal 2650 zcmV-g3Z?ZQiwFoylPzEZ17u-zVJ>QOZ*BnXoNZGY*%HU!-%nxrdP$w<2j6LgnCyZP z7*w{ltn)O23nf#KSQXBknbY0V=l|;-B!BMg=C|XpySKCJ zM{f6*o!x7bOpEt_?(Ft`F+K?V3dgp!%9 zy1Ucb0d5~1G|s5Aby`QAvro}+yBpc}h}Z$|w`qemjy`?%cOzWCs*`!-a+6Qzj=0fq zoP<0qMl%S*YZm0e?<&Jj)S8bFPWc=JIz6UWNQcSqhYt#xW$<{?G2kK zxvPID6aDCBUGs&L7Afb2_KqQ8kd=ZaSdF|JD)Sk4Q3WpLK9lbEMwx)=!3IqCdcEn$Hy6?O>@Elvrm1$6U%lU|Yu|7>OM= z&RQG<4_p=7f@p1H&kd$1#D*;Aend`C&$QCyOVn;AHcifdYd5N4*gI_DY6kn(K&c=lI76^qwM2}%K)fN+QyT=_pXE$R&VAavJ|B#m z(OtLKJ?%utA3inARp;)vsD!OogW03D8psO7W4P%w1PO*xjj_|5%9t8$6>`LKcnQZ$ z48wtO9vp_;aE@WKWb4(Zj>n3g&_+E*o5eBiB4?IiD>Hm&G>`3op)UMyQ$R`t3b={$p*~wWM0jb_T zmMK~p%$SHB?7CRSl3R*F-V|n_bj-!td#;4$9yo)YBphJ`GhI+3hRqGFzJ@E^{UF&47GM4c&#MdcPn`83po+kW2j&#%W>$yvMIty!9g2zNi94`*4Y)}-gdZuhj5`TKWkns0>@Mj)t6VjQ@& zkS)e(fCZ?WCm{$oN56oCp~xy43n zZ<6|9r`1YptaH?uozR0{>!+7DCh3h$r;L2Nb<3>aU=vb-FhMcXP&+xP7=~LxC?XQT zqZP=wi@lDC23-d}Qq5oohXO-td7lmjH|Nx!*~3c~(cYmu9OT{VxY;=Boz`om6rDV_ zPh(4rC6Ka27~@dKpzzR8Ut!4@qlm`ZVz>*8!nWYf8uXo_r?p!N`diaD!AZ z4ys}VbR>a;76k&O7N55uaX-mJ_N~7dLn{!n>(DaS!PmYWi%l{KaQMqKWVeg;5|ds| z&E>o;8q9|u9N%5UH17{X6XxBU7-ys5!#)GEHfDQ7?-BA2itc%zk&l*U@HU})3_20= zPBYeBTy{DMN8s%PyRQ4;ratm>V~T$-=A~vhANf7J9}R~A+NwWxn&CR6)39^+HC)A_ zWAL&+Su{&|HZ86Q%~LXL++V;$*)~n`i`3-(>v_7*54Z2T!iU3?e3XR^zb(*K!u5I| zQ@9MYvWpYP2EzKc|6f~NG(Tg}cD5E>=fab+J?;zVz~rCU{Bw!RM^o4Pp`pfdkK;W| z-$|wB)=#tKLr4S2;V4@yd$`{^Iyl~M9$xH!{(KQNf4lflZB(24?c?U|!jKYy;0 zVMhz!I#rCjI6@(5HvMT8AOG)10JPDL)dntUr#1_8>Zw6TpBZ$M;HFuzomOy8zYWff zL+T4tx0HK3l;c~LdLNoQtX)-cbS=SZw27+8~J1| zWZ5X&;_VILO>FV@;&>}3cbks4l3x6%H9vtH-mK5$@Kb&!$BQ`eXNFr6(p8_xVZQyV zlP%s}DcQOZ*BnXn_E-cI26a<`zgHMx1Gi#`Ib0O%eKI> zvjs{*J8kJ?6k7%_j-A>vTZZX(UrElzNd*Z_vDeIjv|w46qw|-3(s1V7X*#=|QoXO~ zLF(%-HT{Ofg?PTx^e7PJ`IPvyAJC8#F%vbSB%oi#8{;|}lZ1@0D9zI%^Jfhw(Pc(h zlqT!eCG&rDy6BX+RhaMudrC6Pc)k%?`GQrq##YzG)k{+x^HJi|-dN?lwEvPm?gZ z?Dq`By|R^m%NFJD<06WK<6^>}JcPQ~^GwI?+Q{>;bd8+L(Y#B?x3TG3sAEyr>R?RA z9YSrlL$T!&8U~0rEhnzla51vpHM|EzoX>eye}mun$HIo=T95LZ^iyX_7^7Lz-)d%C zd3UAXvM7vb9PBZci5cp5-$WBxCZ9ij7SAl2&;wC+JX=Sk8mCDu+%M2Gqzl{bKng5($@y1_rywD z$d6YKw?&Ny7qi_ry9Rc9J7R-nB*}>{N=V#yU5_FX{Df7$VWc&-On4ucbV3uRCX8d4 z2$|OMO9dGG5MfHv+Q{%s`$q_~YW-%W%aNR?5C_*SPC#!)EKTEFtqn39$Ho=-cU9uJ z?bfE;l%1UBV$b%!d_j0SSFbi)_iU8=Dd&J_#N8=ut{RN++QG=H(XZ2}yeBWKncr8N z3b^EKaaq@1@`=3R14qg1s@mhXQ3v~`*O^^26u*X*!vV7mRL(6h; z=(wTlQ|g5Q4!Z;iTI&b#Ux)7w-{KE(K+eyncs#lK!#~6y-d$b~YQ7&H z3{D1kNDmns;N$)G*VF0wFh$3&&;Kp;j*^H)BpxtvOWSa?l*C>x3@O5Cc}N{S>U@k6 z-sVOwc%2k16=m|+C{36j8S)5~#g+H^4E5qd-@%q0Q2qPkWkS!mp)zHVWQQx-u$TfPFEf)u$ z=-TzG``z`c$myRX7sq@^;+)E|`E}wb>6~WiC0~ZQHk*JZ7d|gxX+}ZC|Fequ4;jlP zE%NeCIGWDHm#EF+Lz+;2qfN82%hyMPS10fG&qjNvr=#<;zek7r$NOh{!;`a(XkUZn z<|~kG8Q8OTB)M4w$rXrJk=)u$a$zfw92oFZNd79FOk>Iy7jV3F8t%w(ewwc>5IG5#c3#f@ebTE&q146TuZ|RuS9*g2Vo=^BPY0 zK8#~vcndY$^%_2x{+UqRse$4OJgX?~0>yy73XEX{!6t|5;Ap|w65bxChaRr@K z5cdG$0DuOAnt<@OCAG4r^%%zEYMbCs&Ig`W%)o%b#ZvqkSMA;6;y}E7Kx&8 zc);#|(wM3R3~z~w5K~1%$UnL5QhV2wp>?s0z(0fYG4Y68RC zq9Ft|s}whDNEB7!Sw(S36a@k_7t{oTw?#h)YA)zqL#Cz*%PM+9rY7wErh}ReFuWxy zLQr#!=A1DXwL0>|5;Ap|w65U(LqQ$=SL#354?0MKMm6A<1O1tF+e zA$Sd$nkqD_2o9N=u>YG4Y68RCq9Ft|S14XXrltzdDvCp;xpe7K!E&4snpyq#x KdKBy*oB#mBztLO( diff --git a/x-pack/test/functional/es_archives/data/search_sessions/mappings.json b/x-pack/test/functional/es_archives/data/search_sessions/mappings.json index a3a56871269df..61305d640fe3e 100644 --- a/x-pack/test/functional/es_archives/data/search_sessions/mappings.json +++ b/x-pack/test/functional/es_archives/data/search_sessions/mappings.json @@ -1,96 +1,8 @@ { "type": "index", "value": { - "aliases": { - ".kibana": { - } - }, - "index": ".kibana_1", + "index": ".kibana", "mappings": { - "_meta": { - "migrationMappingPropertyHashes": { - "action": "6e96ac5e648f57523879661ea72525b7", - "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", - "alert": "49eb3350984bd2a162914d3776e70cfb", - "api_key_pending_invalidation": "16f515278a295f6245149ad7c5ddedb7", - "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", - "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", - "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", - "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", - "background-session": "dfd06597e582fdbbbc09f1a3615e6ce0", - "canvas-element": "7390014e1091044523666d97247392fc", - "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", - "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", - "cases": "477f214ff61acc3af26a7b7818e380c1", - "cases-comments": "8a50736330e953bca91747723a319593", - "cases-configure": "387c5f3a3bda7e0ae0dd4e106f914a69", - "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", - "config": "c63748b75f39d0c54de12d12c1ccbc20", - "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", - "dashboard": "40554caf09725935e2c02e02563a2d07", - "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", - "endpoint:user-artifact-manifest": "a0d7b04ad405eed54d76e279c3727862", - "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "epm-packages": "0cbbb16506734d341a96aaed65ec6413", - "epm-packages-assets": "44621b2f6052ef966da47b7c3a00f33b", - "exception-list": "67f055ab8c10abd7b2ebfd969b836788", - "exception-list-agnostic": "67f055ab8c10abd7b2ebfd969b836788", - "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", - "fleet-agent-actions": "9511b565b1cc6441a42033db3d5de8e9", - "fleet-agent-events": "e20a508b6e805189356be381dbfac8db", - "fleet-agents": "cb661e8ede2b640c42c8e5ef99db0683", - "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7", - "graph-workspace": "27a94b2edcb0610c6aea54a7c56d7752", - "index-pattern": "45915a1ad866812242df474eb0479052", - "infrastructure-ui-source": "3d1b76c39bfb2cc8296b024d73854724", - "ingest-agent-policies": "8b0733cce189659593659dad8db426f0", - "ingest-outputs": "8854f34453a47e26f86a29f8f3b80b4e", - "ingest-package-policies": "c91ca97b1ff700f0fc64dc6b13d65a85", - "ingest_manager_settings": "02a03095f0e05b7a538fa801b88a217f", - "inventory-view": "3d1b76c39bfb2cc8296b024d73854724", - "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "lens": "52346cfec69ff7b47d5f0c12361a2797", - "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", - "map": "4a05b35c3a3a58fbc72dd0202dc3487f", - "maps-telemetry": "5ef305b18111b77789afefbd36b66171", - "metrics-explorer-view": "3d1b76c39bfb2cc8296b024d73854724", - "migrationVersion": "4a1746014a75ade3a714e1db5763276f", - "ml-job": "3bb64c31915acf93fc724af137a0891b", - "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", - "monitoring-telemetry": "2669d5ec15e82391cf58df4294ee9c68", - "namespace": "2f4316de49999235636386fe51dc06c1", - "namespaces": "2f4316de49999235636386fe51dc06c1", - "originId": "2f4316de49999235636386fe51dc06c1", - "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", - "references": "7997cf5a56cc02bdc9c93361bde732b0", - "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "43012c7ebc4cb57054e0a490e4b43023", - "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", - "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", - "siem-ui-timeline": "d12c5474364d737d17252acf1dc4585c", - "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", - "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", - "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", - "spaces-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", - "tag": "83d55da58f6530f7055415717ec06474", - "telemetry": "36a616f7026dfa617d6655df850fe16d", - "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", - "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", - "type": "2f4316de49999235636386fe51dc06c1", - "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", - "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", - "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", - "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", - "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", - "url": "c7f66a0df8b1b52f17c28c4adb111105", - "visualization": "f819cf6636b75c9e76ba733a0c6ef355", - "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" - } - }, "dynamic": "strict", "properties": { "action": { @@ -302,49 +214,6 @@ "dynamic": "false", "type": "object" }, - "search-session": { - "properties": { - "appId": { - "type": "keyword" - }, - "created": { - "type": "date" - }, - "touched": { - "type": "date" - }, - "expires": { - "type": "date" - }, - "idMapping": { - "enabled": false, - "type": "object" - }, - "initialState": { - "enabled": false, - "type": "object" - }, - "name": { - "type": "keyword" - }, - "persisted": { - "type": "boolean" - }, - "restoreState": { - "enabled": false, - "type": "object" - }, - "sessionId": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "urlGeneratorId": { - "type": "keyword" - } - } - }, "canvas-element": { "dynamic": "false", "properties": { @@ -519,6 +388,13 @@ } } }, + "settings": { + "properties": { + "syncAlerts": { + "type": "boolean" + } + } + }, "status": { "type": "keyword" }, @@ -528,6 +404,9 @@ "title": { "type": "keyword" }, + "type": { + "type": "keyword" + }, "updated_at": { "type": "date" }, @@ -551,6 +430,9 @@ "alertId": { "type": "keyword" }, + "associationType": { + "type": "keyword" + }, "comment": { "type": "text" }, @@ -672,6 +554,78 @@ } } }, + "cases-connector-mappings": { + "properties": { + "mappings": { + "properties": { + "action_type": { + "type": "keyword" + }, + "source": { + "type": "keyword" + }, + "target": { + "type": "keyword" + } + } + } + } + }, + "cases-sub-case": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, "cases-user-actions": { "properties": { "action": { @@ -828,6 +782,19 @@ }, "endpoint:user-artifact-manifest": { "properties": { + "artifacts": { + "properties": { + "artifactId": { + "index": false, + "type": "keyword" + }, + "policyId": { + "index": false, + "type": "keyword" + } + }, + "type": "nested" + }, "created": { "index": false, "type": "date" @@ -838,19 +805,6 @@ "semanticVersion": { "index": false, "type": "keyword" - }, - "artifacts": { - "type": "nested", - "properties": { - "policyId": { - "type": "keyword", - "index": false - }, - "artifactId": { - "type": "keyword", - "index": false - } - } } } }, @@ -1053,12 +1007,22 @@ "type": "keyword" }, "name": { + "fields": { + "text": { + "type": "text" + } + }, "type": "keyword" }, "os_types": { "type": "keyword" }, "tags": { + "fields": { + "text": { + "type": "text" + } + }, "type": "keyword" }, "tie_breaker_id": { @@ -1179,12 +1143,22 @@ "type": "keyword" }, "name": { + "fields": { + "text": { + "type": "text" + } + }, "type": "keyword" }, "os_types": { "type": "keyword" }, "tags": { + "fields": { + "text": { + "type": "text" + } + }, "type": "keyword" }, "tie_breaker_id": { @@ -1201,10 +1175,14 @@ } } }, - "file-upload-telemetry": { + "file-upload-usage-collection-telemetry": { "properties": { - "filesUploadedTotalCount": { - "type": "long" + "file_upload": { + "properties": { + "index_creation_count": { + "type": "long" + } + } } } }, @@ -1312,9 +1290,6 @@ "policy_revision": { "type": "integer" }, - "shared_id": { - "type": "keyword" - }, "type": { "type": "keyword" }, @@ -1428,6 +1403,12 @@ "is_default": { "type": "boolean" }, + "is_default_fleet_server": { + "type": "boolean" + }, + "is_managed": { + "type": "boolean" + }, "monitoring_enabled": { "index": false, "type": "keyword" @@ -1622,6 +1603,10 @@ } } }, + "legacy-url-alias": { + "dynamic": "false", + "type": "object" + }, "lens": { "properties": { "description": { @@ -1661,6 +1646,10 @@ }, "map": { "properties": { + "bounds": { + "dynamic": "false", + "type": "object" + }, "description": { "type": "text" }, @@ -1689,47 +1678,6 @@ "dynamic": "false", "type": "object" }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "config": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "index-pattern": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "search": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "space": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, "ml-job": { "properties": { "datafeed_id": { @@ -1753,17 +1701,6 @@ } } }, - "ml-telemetry": { - "properties": { - "file_data_visualizer": { - "properties": { - "index_creation_count": { - "type": "long" - } - } - } - } - }, "monitoring-telemetry": { "properties": { "reportedClusterUuids": { @@ -1843,6 +1780,15 @@ "description": { "type": "text" }, + "grid": { + "enabled": false, + "type": "object" + }, + "hideChart": { + "doc_values": false, + "index": false, + "type": "boolean" + }, "hits": { "doc_values": false, "index": false, @@ -1856,6 +1802,9 @@ } } }, + "pre712": { + "type": "boolean" + }, "sort": { "doc_values": false, "index": false, @@ -1869,6 +1818,58 @@ } } }, + "search-session": { + "properties": { + "appId": { + "type": "keyword" + }, + "created": { + "type": "date" + }, + "expires": { + "type": "date" + }, + "idMapping": { + "enabled": false, + "type": "object" + }, + "initialState": { + "enabled": false, + "type": "object" + }, + "name": { + "type": "keyword" + }, + "persisted": { + "type": "boolean" + }, + "realmName": { + "type": "keyword" + }, + "realmType": { + "type": "keyword" + }, + "restoreState": { + "enabled": false, + "type": "object" + }, + "sessionId": { + "type": "keyword" + }, + "status": { + "type": "keyword" + }, + "touched": { + "type": "date" + }, + "urlGeneratorId": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, "search-telemetry": { "dynamic": "false", "type": "object" @@ -2192,10 +2193,14 @@ "type": "keyword" }, "sort": { + "dynamic": "false", "properties": { "columnId": { "type": "keyword" }, + "columnType": { + "type": "keyword" + }, "sortDirection": { "type": "keyword" } @@ -2389,13 +2394,6 @@ } } }, - "tsvb-validation-telemetry": { - "properties": { - "failedRequests": { - "type": "long" - } - } - }, "type": { "type": "keyword" }, @@ -2604,7 +2602,9 @@ "index": { "auto_expand_replicas": "0-1", "number_of_replicas": "0", - "number_of_shards": "1" + "number_of_shards": "1", + "priority": "10", + "refresh_interval": "1s" } } } diff --git a/x-pack/test/functional/page_objects/search_sessions_management_page.ts b/x-pack/test/functional/page_objects/search_sessions_management_page.ts index df4e99dd595d9..402569971691d 100644 --- a/x-pack/test/functional/page_objects/search_sessions_management_page.ts +++ b/x-pack/test/functional/page_objects/search_sessions_management_page.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { SEARCH_SESSIONS_TABLE_ID } from '../../../plugins/data_enhanced/common/search'; import { FtrProviderContext } from '../ftr_provider_context'; export function SearchSessionsPageProvider({ getService, getPageObjects }: FtrProviderContext) { @@ -23,7 +24,7 @@ export function SearchSessionsPageProvider({ getService, getPageObjects }: FtrPr }, async getList() { - const table = await testSubjects.find('searchSessionsMgmtTable'); + const table = await testSubjects.find(SEARCH_SESSIONS_TABLE_ID); const allRows = await table.findAllByTestSubject('searchSessionsRow'); return Promise.all( @@ -45,9 +46,7 @@ export function SearchSessionsPageProvider({ getService, getPageObjects }: FtrPr reload: async () => { log.debug('management ui: reload the status'); await actionsCell.click(); - await find.clickByCssSelector( - '[data-test-subj="sessionManagementPopoverAction-reload"]' - ); + await testSubjects.click('sessionManagementPopoverAction-reload'); }, delete: async () => { log.debug('management ui: delete the session'); diff --git a/x-pack/test/send_search_to_background_integration/config.ts b/x-pack/test/send_search_to_background_integration/config.ts index cc09fe8b568e0..2763ebb63c3ef 100644 --- a/x-pack/test/send_search_to_background_integration/config.ts +++ b/x-pack/test/send_search_to_background_integration/config.ts @@ -24,8 +24,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { testFiles: [ resolve(__dirname, './tests/apps/dashboard/async_search'), resolve(__dirname, './tests/apps/discover'), - resolve(__dirname, './tests/apps/management/search_sessions'), resolve(__dirname, './tests/apps/lens'), + resolve(__dirname, './tests/apps/management/search_sessions'), ], kbnTestServer: { diff --git a/x-pack/test/send_search_to_background_integration/services/search_sessions.ts b/x-pack/test/send_search_to_background_integration/services/search_sessions.ts index bf79d35178a60..0d03a28dfb901 100644 --- a/x-pack/test/send_search_to_background_integration/services/search_sessions.ts +++ b/x-pack/test/send_search_to_background_integration/services/search_sessions.ts @@ -137,7 +137,9 @@ export function SearchSessionsProvider({ getService }: FtrProviderContext) { .expect(200); const { saved_objects: savedObjects } = body as SavedObjectsFindResponse; - log.debug(`Found created search sessions: ${savedObjects.map(({ id }) => id)}`); + if (savedObjects.length) { + log.debug(`Found created search sessions: ${savedObjects.map(({ id }) => id)}`); + } await Promise.all( savedObjects.map(async (so) => { log.debug(`Deleting search session: ${so.id}`); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/index.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/index.ts index 5a912117fe445..82642a640ce47 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/index.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/index.ts @@ -13,7 +13,7 @@ export default function ({ loadTestFile, getService, getPageObjects }: FtrProvid const PageObjects = getPageObjects(['common']); const searchSessions = getService('searchSessions'); - describe('async search', function () { + describe('Dashboard', function () { this.tags('ciGroup3'); before(async () => { diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/discover/index.ts b/x-pack/test/send_search_to_background_integration/tests/apps/discover/index.ts index 42f7560b82f4f..f2bbdf9c9287b 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/discover/index.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/discover/index.ts @@ -13,7 +13,7 @@ export default function ({ loadTestFile, getService, getPageObjects }: FtrProvid const PageObjects = getPageObjects(['common']); const searchSessions = getService('searchSessions'); - describe('async search', function () { + describe('Discover', function () { this.tags('ciGroup3'); before(async () => { diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts index f925cfb78a8c6..d81a7ee12f616 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts @@ -22,13 +22,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const log = getService('log'); - // FLAKY: https://github.com/elastic/kibana/issues/89069 - describe.skip('Search sessions Management UI', () => { + describe('Search Sessions Management UI', () => { describe('New search sessions', () => { before(async () => { await PageObjects.common.navigateToApp('dashboard'); log.debug('wait for dashboard landing page'); - retry.tryForTime(10000, async () => { + await retry.tryForTime(10000, async () => { testSubjects.existOrFail('dashboardLandingPage'); }); await searchSessions.markTourDone(); @@ -51,6 +50,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.waitFor(`wait for first item to complete`, async function () { const s = await PageObjects.searchSessionsManagement.getList(); + if (!s[0]) { + log.warning(`Expected item is not in the table!`); + } else { + log.debug(`First item status: ${s[0].status}`); + } return s[0] && s[0].status === 'complete'; }); @@ -72,22 +76,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await searchSessions.expectState('restored'); }); - // NOTE: this test depends on the previous one passing - it('Reloads as new session from management', async () => { - await PageObjects.searchSessionsManagement.goTo(); - - const searchSessionList = await PageObjects.searchSessionsManagement.getList(); - - expect(searchSessionList.length).to.be(1); - await searchSessionList[0].reload(); - - // embeddable has loaded - await PageObjects.dashboard.waitForRenderComplete(); - - // new search session was completed - await searchSessions.expectState('completed'); - }); - it('Deletes a session from management', async () => { await PageObjects.searchSessionsManagement.goTo(); @@ -122,34 +110,105 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.load('data/search_sessions'); const searchSessionList = await PageObjects.searchSessionsManagement.getList(); - expect(searchSessionList.length).to.be(10); + expectSnapshot(searchSessionList.map((ss) => [ss.app, ss.name, ss.created, ss.expires])) + .toMatchInline(` + Array [ + Array [ + "graph", + "[eCommerce] Orders Test 6 ", + "16 Feb, 2021, 00:00:00", + "--", + ], + Array [ + "lens", + "[eCommerce] Orders Test 7", + "15 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "apm", + "[eCommerce] Orders Test 8", + "14 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "appSearch", + "[eCommerce] Orders Test 9", + "13 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "auditbeat", + "[eCommerce] Orders Test 10", + "12 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "code", + "[eCommerce] Orders Test 11", + "11 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "console", + "[eCommerce] Orders Test 12", + "10 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "security", + "[eCommerce] Orders Test 5 ", + "9 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "visualize", + "[eCommerce] Orders Test 4 ", + "8 Feb, 2021, 00:00:00", + "--", + ], + Array [ + "canvas", + "[eCommerce] Orders Test 3", + "7 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + ] + `); + + await esArchiver.unload('data/search_sessions'); + }); + + it('has working pagination controls', async () => { + await esArchiver.load('data/search_sessions'); - expect(searchSessionList.map((ss) => ss.created)).to.eql([ - '25 Dec, 2020, 00:00:00', - '24 Dec, 2020, 00:00:00', - '23 Dec, 2020, 00:00:00', - '22 Dec, 2020, 00:00:00', - '21 Dec, 2020, 00:00:00', - '20 Dec, 2020, 00:00:00', - '19 Dec, 2020, 00:00:00', - '18 Dec, 2020, 00:00:00', - '17 Dec, 2020, 00:00:00', - '16 Dec, 2020, 00:00:00', - ]); - - expect(searchSessionList.map((ss) => ss.expires)).to.eql([ - '--', - '--', - '--', - '23 Dec, 2020, 00:00:00', - '22 Dec, 2020, 00:00:00', - '--', - '--', - '--', - '18 Dec, 2020, 00:00:00', - '17 Dec, 2020, 00:00:00', - ]); + log.debug(`loading first page of sessions`); + const sessionListFirst = await PageObjects.searchSessionsManagement.getList(); + expect(sessionListFirst.length).to.be(10); + + await testSubjects.click('pagination-button-next'); + + const sessionListSecond = await PageObjects.searchSessionsManagement.getList(); + expect(sessionListSecond.length).to.be(2); + + expectSnapshot(sessionListSecond.map((ss) => [ss.app, ss.name, ss.created, ss.expires])) + .toMatchInline(` + Array [ + Array [ + "discover", + "[eCommerce] Orders Test 2", + "6 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + Array [ + "dashboard", + "[eCommerce] Revenue Dashboard", + "5 Feb, 2021, 00:00:00", + "24 Feb, 2021, 00:00:00", + ], + ] + `); await esArchiver.unload('data/search_sessions'); }); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management_permissions.ts b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management_permissions.ts index 48f4156afbe82..ad22fd2cbaf71 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management_permissions.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management_permissions.ts @@ -22,8 +22,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const appsMenu = getService('appsMenu'); const managementMenu = getService('managementMenu'); - describe('Search sessions Management UI permissions', () => { - describe('Sessions management is not available if non of apps enable search sessions', () => { + describe('Search Sessions Management UI permissions', () => { + describe('Sessions management is not available', () => { before(async () => { await security.role.create('data_analyst', { elasticsearch: {}, @@ -56,13 +56,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.security.forceLogout(); }); - it('Sessions management is not available if non of apps enable search sessions', async () => { + it('if no apps enable search sessions', async () => { const links = await appsMenu.readLinks(); expect(links.map((link) => link.text)).to.not.contain('Stack Management'); }); }); - describe('Sessions management is available if one of apps enables search sessions', () => { + describe('Sessions management is available', () => { before(async () => { await security.role.create('data_analyst', { elasticsearch: {}, @@ -95,7 +95,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.security.forceLogout(); }); - it('Sessions management is available if one of apps enables search sessions', async () => { + it('if one app enables search sessions', async () => { const links = await appsMenu.readLinks(); expect(links.map((link) => link.text)).to.contain('Stack Management'); await PageObjects.common.navigateToApp('management'); From 85bc8b0b42d4a6d343d12b33aae71f5db42ae852 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Fri, 19 Feb 2021 12:34:01 -0500 Subject: [PATCH 15/32] [Time to Visualize] Stay in Edit Mode After Dashboard Quicksave (#91729) * Make quicksave function stay in edit mode --- .../public/application/dashboard_app.tsx | 1 + .../application/dashboard_state_manager.ts | 11 ++- .../public/application/lib/save_dashboard.ts | 6 +- .../application/listing/confirm_overlays.tsx | 47 +++++----- .../listing/dashboard_unsaved_listing.tsx | 18 ++-- .../application/top_nav/dashboard_top_nav.tsx | 27 +++--- .../application/top_nav/get_top_nav_config.ts | 54 ++++++----- .../dashboard/public/dashboard_strings.ts | 90 ++++++++++++------- .../public/top_nav_menu/top_nav_menu_data.tsx | 1 + .../public/top_nav_menu/top_nav_menu_item.tsx | 1 + .../apps/dashboard/dashboard_save.ts | 6 +- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 13 files changed, 156 insertions(+), 110 deletions(-) diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index f659fa002e922..8466cf009db9d 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -294,6 +294,7 @@ export function DashboardApp({ }} viewMode={viewMode} lastDashboardId={savedDashboardId} + clearUnsavedChanges={() => setUnsavedChanges(false)} timefilter={data.query.timefilter.timefilter} onQuerySubmit={(_payload, isUpdate) => { if (isUpdate === false) { diff --git a/src/plugins/dashboard/public/application/dashboard_state_manager.ts b/src/plugins/dashboard/public/application/dashboard_state_manager.ts index e4b2afa8a46ea..7f3f347e6e3ae 100644 --- a/src/plugins/dashboard/public/application/dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/dashboard_state_manager.ts @@ -345,7 +345,7 @@ export class DashboardStateManager { /** * Resets the state back to the last saved version of the dashboard. */ - public resetState() { + public resetState(resetViewMode: boolean) { // In order to show the correct warning, we have to store the unsaved // title on the dashboard object. We should fix this at some point, but this is how all the other object // save panels work at the moment. @@ -366,9 +366,14 @@ export class DashboardStateManager { this.stateDefaults.query = this.lastSavedDashboardFilters.query; // Need to make a copy to ensure they are not overwritten. this.stateDefaults.filters = [...this.getLastSavedFilterBars()]; - this.isDirty = false; - this.stateContainer.set(this.stateDefaults); + + if (resetViewMode) { + this.stateContainer.set(this.stateDefaults); + } else { + const currentViewMode = this.stateContainer.get().viewMode; + this.stateContainer.set({ ...this.stateDefaults, viewMode: currentViewMode }); + } } /** diff --git a/src/plugins/dashboard/public/application/lib/save_dashboard.ts b/src/plugins/dashboard/public/application/lib/save_dashboard.ts index 80392f61946cd..6913fcda4c8e2 100644 --- a/src/plugins/dashboard/public/application/lib/save_dashboard.ts +++ b/src/plugins/dashboard/public/application/lib/save_dashboard.ts @@ -11,6 +11,8 @@ import { SavedObjectSaveOpts } from '../../services/saved_objects'; import { updateSavedDashboard } from './update_saved_dashboard'; import { DashboardStateManager } from '../dashboard_state_manager'; +export type SavedDashboardSaveOpts = SavedObjectSaveOpts & { stayInEditMode?: boolean }; + /** * Saves the dashboard. * @param toJson A custom toJson function. Used because the previous code used @@ -23,7 +25,7 @@ export function saveDashboard( toJson: (obj: any) => string, timeFilter: TimefilterContract, dashboardStateManager: DashboardStateManager, - saveOptions: SavedObjectSaveOpts + saveOptions: SavedDashboardSaveOpts ): Promise { const savedDashboard = dashboardStateManager.savedDashboard; const appState = dashboardStateManager.appState; @@ -36,7 +38,7 @@ export function saveDashboard( // reset state only when save() was successful // e.g. save() could be interrupted if title is duplicated and not confirmed dashboardStateManager.lastSavedDashboardFilters = dashboardStateManager.getFilterState(); - dashboardStateManager.resetState(); + dashboardStateManager.resetState(!saveOptions.stayInEditMode); } return id; diff --git a/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx b/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx index d302bb4216bc4..b1e9af32ccd19 100644 --- a/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx +++ b/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx @@ -18,21 +18,23 @@ import { } from '@elastic/eui'; import React from 'react'; import { OverlayStart } from '../../../../../core/public'; -import { createConfirmStrings, leaveConfirmStrings } from '../../dashboard_strings'; +import { + createConfirmStrings, + discardConfirmStrings, + leaveEditModeConfirmStrings, +} from '../../dashboard_strings'; import { toMountPoint } from '../../services/kibana_react'; -export const confirmDiscardUnsavedChanges = ( - overlays: OverlayStart, - discardCallback: () => void, - cancelButtonText = leaveConfirmStrings.getCancelButtonText() -) => +export type DiscardOrKeepSelection = 'cancel' | 'discard' | 'keep'; + +export const confirmDiscardUnsavedChanges = (overlays: OverlayStart, discardCallback: () => void) => overlays - .openConfirm(leaveConfirmStrings.getDiscardSubtitle(), { - confirmButtonText: leaveConfirmStrings.getConfirmButtonText(), - cancelButtonText, + .openConfirm(discardConfirmStrings.getDiscardSubtitle(), { + confirmButtonText: discardConfirmStrings.getDiscardConfirmButtonText(), + cancelButtonText: discardConfirmStrings.getDiscardCancelButtonText(), buttonColor: 'danger', defaultFocusedButton: EUI_MODAL_CANCEL_BUTTON, - title: leaveConfirmStrings.getDiscardTitle(), + title: discardConfirmStrings.getDiscardTitle(), }) .then((isConfirmed) => { if (isConfirmed) { @@ -40,8 +42,6 @@ export const confirmDiscardUnsavedChanges = ( } }); -export type DiscardOrKeepSelection = 'cancel' | 'discard' | 'keep'; - export const confirmDiscardOrKeepUnsavedChanges = ( overlays: OverlayStart ): Promise => { @@ -50,11 +50,13 @@ export const confirmDiscardOrKeepUnsavedChanges = ( toMountPoint( <> - {leaveConfirmStrings.getLeaveEditModeTitle()} + + {leaveEditModeConfirmStrings.getLeaveEditModeTitle()} + - {leaveConfirmStrings.getLeaveEditModeSubtitle()} + {leaveEditModeConfirmStrings.getLeaveEditModeSubtitle()} @@ -62,33 +64,34 @@ export const confirmDiscardOrKeepUnsavedChanges = ( data-test-subj="dashboardDiscardConfirmCancel" onClick={() => session.close()} > - {leaveConfirmStrings.getCancelButtonText()} + {leaveEditModeConfirmStrings.getLeaveEditModeCancelButtonText()} { session.close(); - resolve('keep'); + resolve('discard'); }} > - {leaveConfirmStrings.getKeepChangesText()} + {leaveEditModeConfirmStrings.getLeaveEditModeDiscardButtonText()} { session.close(); - resolve('discard'); + resolve('keep'); }} > - {leaveConfirmStrings.getConfirmButtonText()} + {leaveEditModeConfirmStrings.getLeaveEditModeKeepChangesText()} ), { 'data-test-subj': 'dashboardDiscardConfirmModal', + maxWidth: 550, } ); }); diff --git a/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx b/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx index db50cfb638d64..66e8b2348490a 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_unsaved_listing.tsx @@ -17,11 +17,7 @@ import { } from '@elastic/eui'; import React, { useCallback, useEffect, useState } from 'react'; import { DashboardSavedObject } from '../..'; -import { - createConfirmStrings, - dashboardUnsavedListingStrings, - getNewDashboardTitle, -} from '../../dashboard_strings'; +import { dashboardUnsavedListingStrings, getNewDashboardTitle } from '../../dashboard_strings'; import { useKibana } from '../../services/kibana_react'; import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_panel_storage'; import { DashboardAppServices, DashboardRedirect } from '../types'; @@ -136,14 +132,10 @@ export const DashboardUnsavedListing = ({ const onDiscard = useCallback( (id?: string) => { - confirmDiscardUnsavedChanges( - overlays, - () => { - dashboardPanelStorage.clearPanels(id); - refreshUnsavedDashboards(); - }, - createConfirmStrings.getCancelButtonText() - ); + confirmDiscardUnsavedChanges(overlays, () => { + dashboardPanelStorage.clearPanels(id); + refreshUnsavedDashboards(); + }); }, [overlays, refreshUnsavedDashboards, dashboardPanelStorage] ); diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 11fb7f0cb56ff..d279a6c219c9d 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -19,12 +19,7 @@ import { openAddPanelFlyout, ViewMode, } from '../../services/embeddable'; -import { - getSavedObjectFinder, - SavedObjectSaveOpts, - SaveResult, - showSaveModal, -} from '../../services/saved_objects'; +import { getSavedObjectFinder, SaveResult, showSaveModal } from '../../services/saved_objects'; import { NavAction } from '../../types'; import { DashboardSavedObject } from '../..'; @@ -48,6 +43,7 @@ import { OverlayRef } from '../../../../../core/public'; import { getNewDashboardTitle, unsavedChangesBadge } from '../../dashboard_strings'; import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_panel_storage'; import { DashboardContainer } from '..'; +import { SavedDashboardSaveOpts } from '../lib/save_dashboard'; export interface DashboardTopNavState { chromeIsVisible: boolean; @@ -64,13 +60,15 @@ export interface DashboardTopNavProps { timefilter: TimefilterContract; indexPatterns: IndexPattern[]; redirectTo: DashboardRedirect; - unsavedChanges?: boolean; + unsavedChanges: boolean; + clearUnsavedChanges: () => void; lastDashboardId?: string; viewMode: ViewMode; } export function DashboardTopNav({ dashboardStateManager, + clearUnsavedChanges, dashboardContainer, lastDashboardId, unsavedChanges, @@ -98,6 +96,7 @@ export function DashboardTopNav({ } = useKibana().services; const [state, setState] = useState({ chromeIsVisible: false }); + const [isSaveInProgress, setIsSaveInProgress] = useState(false); useEffect(() => { const visibleSubscription = chrome.getIsVisible$().subscribe((chromeIsVisible) => { @@ -177,7 +176,7 @@ export function DashboardTopNav({ } function discardChanges() { - dashboardStateManager.resetState(); + dashboardStateManager.resetState(true); dashboardStateManager.clearUnsavedPanels(); // We need to do a hard reset of the timepicker. appState will not reload like @@ -222,7 +221,7 @@ export function DashboardTopNav({ * @resolved {String} - The id of the doc */ const save = useCallback( - async (saveOptions: SavedObjectSaveOpts) => { + async (saveOptions: SavedDashboardSaveOpts) => { return saveDashboard(angular.toJson, timefilter, dashboardStateManager, saveOptions) .then(function (id) { if (id) { @@ -239,7 +238,6 @@ export function DashboardTopNav({ redirectTo({ destination: 'dashboard', id, useReplace: !lastDashboardId }); } else { chrome.docTitle.change(dashboardStateManager.savedDashboard.lastSavedTitle); - dashboardStateManager.switchViewMode(ViewMode.VIEW); } } return { id }; @@ -355,7 +353,8 @@ export function DashboardTopNav({ } } - save({}).then((response: SaveResult) => { + setIsSaveInProgress(true); + save({ stayInEditMode: true }).then((response: SaveResult) => { // If the save wasn't successful, put the original values back. if (!(response as { id: string }).id) { dashboardStateManager.setTitle(currentTitle); @@ -364,10 +363,13 @@ export function DashboardTopNav({ if (savedObjectsTagging) { dashboardStateManager.setTags(currentTags); } + } else { + clearUnsavedChanges(); } + setIsSaveInProgress(false); return response; }); - }, [save, savedObjectsTagging, dashboardStateManager]); + }, [save, savedObjectsTagging, dashboardStateManager, clearUnsavedChanges]); const runClone = useCallback(() => { const currentTitle = dashboardStateManager.getTitle(); @@ -467,6 +469,7 @@ export function DashboardTopNav({ hideWriteControls: dashboardCapabilities.hideWriteControls, isNewDashboard: !savedDashboard.id, isDirty: dashboardStateManager.isDirty, + isSaveInProgress, }); const badges = unsavedChanges diff --git a/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts b/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts index 26eea1b5f718d..801ab54eb9839 100644 --- a/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts +++ b/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { ViewMode } from '../../services/embeddable'; import { TopNavIds } from './top_nav_ids'; import { NavAction } from '../../types'; +import { TopNavMenuData } from '../../../../navigation/public'; /** * @param actions - A mapping of TopNavIds to an action function that should run when the @@ -20,7 +21,12 @@ import { NavAction } from '../../types'; export function getTopNavConfig( dashboardMode: ViewMode, actions: { [key: string]: NavAction }, - options: { hideWriteControls: boolean; isNewDashboard: boolean; isDirty: boolean } + options: { + hideWriteControls: boolean; + isNewDashboard: boolean; + isDirty: boolean; + isSaveInProgress?: boolean; + } ) { switch (dashboardMode) { case ViewMode.VIEW: @@ -36,20 +42,17 @@ export function getTopNavConfig( getEditConfig(actions[TopNavIds.ENTER_EDIT_MODE]), ]; case ViewMode.EDIT: - return options.isNewDashboard - ? [ - getOptionsConfig(actions[TopNavIds.OPTIONS]), - getShareConfig(actions[TopNavIds.SHARE]), - getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]), - getSaveConfig(actions[TopNavIds.SAVE], options.isNewDashboard), - ] - : [ - getOptionsConfig(actions[TopNavIds.OPTIONS]), - getShareConfig(actions[TopNavIds.SHARE]), - getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]), - getSaveConfig(actions[TopNavIds.SAVE]), - getQuickSave(actions[TopNavIds.QUICK_SAVE]), - ]; + const disableButton = options.isSaveInProgress; + const navItems: TopNavMenuData[] = [ + getOptionsConfig(actions[TopNavIds.OPTIONS], disableButton), + getShareConfig(actions[TopNavIds.SHARE], disableButton), + getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE], disableButton), + getSaveConfig(actions[TopNavIds.SAVE], options.isNewDashboard, disableButton), + ]; + if (!options.isNewDashboard) { + navItems.push(getQuickSave(actions[TopNavIds.QUICK_SAVE], disableButton, options.isDirty)); + } + return navItems; default: return []; } @@ -106,9 +109,12 @@ function getEditConfig(action: NavAction) { /** * @returns {kbnTopNavConfig} */ -function getQuickSave(action: NavAction) { +function getQuickSave(action: NavAction, isLoading?: boolean, isDirty?: boolean) { return { + isLoading, + disableButton: !isDirty, id: 'quick-save', + iconType: 'save', emphasize: true, label: getSaveButtonLabel(), description: i18n.translate('dashboard.topNave.saveConfigDescription', { @@ -122,10 +128,12 @@ function getQuickSave(action: NavAction) { /** * @returns {kbnTopNavConfig} */ -function getSaveConfig(action: NavAction, isNewDashboard = false) { +function getSaveConfig(action: NavAction, isNewDashboard = false, disableButton?: boolean) { return { + disableButton, id: 'save', label: isNewDashboard ? getSaveButtonLabel() : getSaveAsButtonLabel(), + iconType: isNewDashboard ? 'save' : undefined, description: i18n.translate('dashboard.topNave.saveAsConfigDescription', { defaultMessage: 'Save as a new dashboard', }), @@ -138,11 +146,12 @@ function getSaveConfig(action: NavAction, isNewDashboard = false) { /** * @returns {kbnTopNavConfig} */ -function getViewConfig(action: NavAction) { +function getViewConfig(action: NavAction, disableButton?: boolean) { return { + disableButton, id: 'cancel', label: i18n.translate('dashboard.topNave.cancelButtonAriaLabel', { - defaultMessage: 'cancel', + defaultMessage: 'Return', }), description: i18n.translate('dashboard.topNave.viewConfigDescription', { defaultMessage: 'Switch to view-only mode', @@ -172,7 +181,7 @@ function getCloneConfig(action: NavAction) { /** * @returns {kbnTopNavConfig} */ -function getShareConfig(action: NavAction | undefined) { +function getShareConfig(action: NavAction | undefined, disableButton?: boolean) { return { id: 'share', label: i18n.translate('dashboard.topNave.shareButtonAriaLabel', { @@ -184,15 +193,16 @@ function getShareConfig(action: NavAction | undefined) { testId: 'shareTopNavButton', run: action ?? (() => {}), // disable the Share button if no action specified - disableButton: !action, + disableButton: !action || disableButton, }; } /** * @returns {kbnTopNavConfig} */ -function getOptionsConfig(action: NavAction) { +function getOptionsConfig(action: NavAction, disableButton?: boolean) { return { + disableButton, id: 'options', label: i18n.translate('dashboard.topNave.optionsButtonAriaLabel', { defaultMessage: 'options', diff --git a/src/plugins/dashboard/public/dashboard_strings.ts b/src/plugins/dashboard/public/dashboard_strings.ts index dad347b176c7e..79a59d0cfa605 100644 --- a/src/plugins/dashboard/public/dashboard_strings.ts +++ b/src/plugins/dashboard/public/dashboard_strings.ts @@ -199,6 +199,25 @@ export const getNewDashboardTitle = () => defaultMessage: 'New Dashboard', }); +export const getDashboard60Warning = () => + i18n.translate('dashboard.urlWasRemovedInSixZeroWarningMessage', { + defaultMessage: 'The url "dashboard/create" was removed in 6.0. Please update your bookmarks.', + }); + +export const dashboardReadonlyBadge = { + getText: () => + i18n.translate('dashboard.badge.readOnly.text', { + defaultMessage: 'Read only', + }), + getTooltip: () => + i18n.translate('dashboard.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save dashboards', + }), +}; + +/* + Modals +*/ export const shareModalStrings = { getTopMenuCheckbox: () => i18n.translate('dashboard.embedUrlParamExtension.topMenu', { @@ -222,22 +241,6 @@ export const shareModalStrings = { }), }; -export const getDashboard60Warning = () => - i18n.translate('dashboard.urlWasRemovedInSixZeroWarningMessage', { - defaultMessage: 'The url "dashboard/create" was removed in 6.0. Please update your bookmarks.', - }); - -export const dashboardReadonlyBadge = { - getText: () => - i18n.translate('dashboard.badge.readOnly.text', { - defaultMessage: 'Read only', - }), - getTooltip: () => - i18n.translate('dashboard.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save dashboards', - }), -}; - export const leaveConfirmStrings = { getLeaveTitle: () => i18n.translate('dashboard.appLeaveConfirmModal.unsavedChangesTitle', { @@ -247,33 +250,51 @@ export const leaveConfirmStrings = { i18n.translate('dashboard.appLeaveConfirmModal.unsavedChangesSubtitle', { defaultMessage: 'Leave Dashboard with unsaved work?', }), - getKeepChangesText: () => - i18n.translate('dashboard.appLeaveConfirmModal.keepUnsavedChangesButtonLabel', { - defaultMessage: 'Keep unsaved changes', + getLeaveCancelButtonText: () => + i18n.translate('dashboard.appLeaveConfirmModal.cancelButtonLabel', { + defaultMessage: 'Cancel', }), +}; + +export const leaveEditModeConfirmStrings = { getLeaveEditModeTitle: () => - i18n.translate('dashboard.changeViewModeConfirmModal.leaveEditMode', { - defaultMessage: 'Leave edit mode with unsaved work?', + i18n.translate('dashboard.changeViewModeConfirmModal.leaveEditModeTitle', { + defaultMessage: 'You have unsaved changes', }), getLeaveEditModeSubtitle: () => - i18n.translate('dashboard.changeViewModeConfirmModal.discardChangesOptionalDescription', { - defaultMessage: `If you discard your changes, there's no getting them back.`, + i18n.translate('dashboard.changeViewModeConfirmModal.description', { + defaultMessage: `You can keep or discard your changes on return to view mode. You can't recover discarded changes.`, + }), + getLeaveEditModeKeepChangesText: () => + i18n.translate('dashboard.changeViewModeConfirmModal.keepUnsavedChangesButtonLabel', { + defaultMessage: 'Keep changes', + }), + getLeaveEditModeDiscardButtonText: () => + i18n.translate('dashboard.changeViewModeConfirmModal.confirmButtonLabel', { + defaultMessage: 'Discard changes', + }), + getLeaveEditModeCancelButtonText: () => + i18n.translate('dashboard.changeViewModeConfirmModal.cancelButtonLabel', { + defaultMessage: 'Continue editing', }), +}; + +export const discardConfirmStrings = { getDiscardTitle: () => - i18n.translate('dashboard.changeViewModeConfirmModal.discardChangesTitle', { + i18n.translate('dashboard.discardChangesConfirmModal.discardChangesTitle', { defaultMessage: 'Discard changes to dashboard?', }), getDiscardSubtitle: () => - i18n.translate('dashboard.changeViewModeConfirmModal.discardChangesDescription', { + i18n.translate('dashboard.discardChangesConfirmModal.discardChangesDescription', { defaultMessage: `Once you discard your changes, there's no getting them back.`, }), - getConfirmButtonText: () => - i18n.translate('dashboard.changeViewModeConfirmModal.confirmButtonLabel', { + getDiscardConfirmButtonText: () => + i18n.translate('dashboard.discardChangesConfirmModal.confirmButtonLabel', { defaultMessage: 'Discard changes', }), - getCancelButtonText: () => - i18n.translate('dashboard.changeViewModeConfirmModal.cancelButtonLabel', { - defaultMessage: 'Continue editing', + getDiscardCancelButtonText: () => + i18n.translate('dashboard.discardChangesConfirmModal.cancelButtonLabel', { + defaultMessage: 'Cancel', }), }; @@ -290,13 +311,20 @@ export const createConfirmStrings = { i18n.translate('dashboard.createConfirmModal.confirmButtonLabel', { defaultMessage: 'Start over', }), - getContinueButtonText: () => leaveConfirmStrings.getCancelButtonText(), + getContinueButtonText: () => + i18n.translate('dashboard.createConfirmModal.continueButtonLabel', { + defaultMessage: 'Continue editing', + }), getCancelButtonText: () => i18n.translate('dashboard.createConfirmModal.cancelButtonLabel', { defaultMessage: 'Cancel', }), }; +/* + Error Messages +*/ + export const panelStorageErrorStrings = { getPanelsGetError: (message: string) => i18n.translate('dashboard.panelStorageError.getError', { diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx index 3a54c7ed01185..b6b056134361a 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx @@ -20,6 +20,7 @@ export interface TopNavMenuData { disableButton?: boolean | (() => boolean); tooltip?: string | (() => string | undefined); emphasize?: boolean; + isLoading?: boolean; iconType?: string; iconSide?: EuiButtonProps['iconSide']; } diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx index ec91452badf36..523bf07f828c9 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx @@ -30,6 +30,7 @@ export function TopNavMenuItem(props: TopNavMenuData) { const commonButtonProps = { isDisabled: isDisabled(), onClick: handleClick, + isLoading: props.isLoading, iconType: props.iconType, iconSide: props.iconSide, 'data-test-subj': props.testId, diff --git a/test/functional/apps/dashboard/dashboard_save.ts b/test/functional/apps/dashboard/dashboard_save.ts index d1320b064b6d1..0a0a2fc1dd286 100644 --- a/test/functional/apps/dashboard/dashboard_save.ts +++ b/test/functional/apps/dashboard/dashboard_save.ts @@ -130,7 +130,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.clickQuickSave(); await testSubjects.existOrFail('saveDashboardSuccess'); - await testSubjects.existOrFail('dashboardEditMode'); + }); + + it('Stays in edit mode after performing a quick save', async function () { + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('dashboardQuickSaveMenuItem'); }); }); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1162a9bf00c70..16712da1d7b2e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -589,8 +589,6 @@ "dashboard.badge.readOnly.tooltip": "ダッシュボードを保存できません", "dashboard.changeViewModeConfirmModal.cancelButtonLabel": "編集を続行", "dashboard.changeViewModeConfirmModal.confirmButtonLabel": "変更を破棄", - "dashboard.changeViewModeConfirmModal.discardChangesDescription": "変更を破棄すると、元に戻すことはできません。", - "dashboard.changeViewModeConfirmModal.discardChangesTitle": "ダッシュボードへの変更を破棄しますか?", "dashboard.cloneModal.cloneDashboardTitleAriaLabel": "クローンダッシュボードタイトル", "dashboard.dashboardAppBreadcrumbsTitle": "ダッシュボード", "dashboard.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "ダッシュボードが読み込めません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fc658ae8ce719..e89fc62a21db6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -589,8 +589,6 @@ "dashboard.badge.readOnly.tooltip": "无法保存仪表板", "dashboard.changeViewModeConfirmModal.cancelButtonLabel": "继续编辑", "dashboard.changeViewModeConfirmModal.confirmButtonLabel": "放弃更改", - "dashboard.changeViewModeConfirmModal.discardChangesDescription": "放弃更改后,它们将无法恢复。", - "dashboard.changeViewModeConfirmModal.discardChangesTitle": "放弃对仪表板的更改?", "dashboard.cloneModal.cloneDashboardTitleAriaLabel": "克隆仪表板标题", "dashboard.dashboardAppBreadcrumbsTitle": "仪表板", "dashboard.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "无法加载仪表板。", From 857300b1477ac3ab6cea1988dadc9997e2780d39 Mon Sep 17 00:00:00 2001 From: Brandon Morelli Date: Fri, 19 Feb 2021 09:51:10 -0800 Subject: [PATCH 16/32] docs: update dependencies table bug (#91964) --- docs/apm/service-overview.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/apm/service-overview.asciidoc b/docs/apm/service-overview.asciidoc index 5fd214e6ce613..36d021d64456e 100644 --- a/docs/apm/service-overview.asciidoc +++ b/docs/apm/service-overview.asciidoc @@ -62,8 +62,8 @@ each dependency. By default, dependencies are sorted by _Impact_ to show the mos If there is a particular dependency you are interested in, click *View service map* to view the related <>. -IMPORTANT: A known issue prevents Real User Monitoring (RUM) dependencies from being shown in the -*Dependencies* table. We are working on a fix for this issue. +NOTE: Displaying dependencies for services instrumented with the Real User Monitoring (RUM) agent +requires an agent version ≥ v5.6.3. [role="screenshot"] image::apm/images/spans-dependencies.png[Span type duration and dependencies] From 405255cd9704079ede5a5c7c2274e90731de6b7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 19 Feb 2021 12:55:29 -0500 Subject: [PATCH 17/32] [APM] Break down error table api removing the sparklines (#89138) * breaking error table api * shows loading state while fetching metrics * adding api tests * removing pagination from server * adding API test * refactoring * fixing license * renaming apis * fixing some stuff * addressing PR comments * adding request id * addressing PR comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Dario Gieselaar --- .../service_overview.test.tsx | 3 +- .../get_column.tsx | 94 +++++++ .../service_overview_errors_table/index.tsx | 197 ++++++--------- ...rvice_error_group_comparison_statistics.ts | 105 ++++++++ ..._service_error_group_primary_statistics.ts | 96 ++++++++ .../apm/server/routes/create_apm_api.ts | 6 +- x-pack/plugins/apm/server/routes/services.ts | 70 ++++-- .../plugins/apm/server/routes/transactions.ts | 2 +- .../test/apm_api_integration/tests/index.ts | 3 +- .../tests/service_overview/error_groups.ts | 229 ------------------ .../error_groups_comparison_statistics.snap | 133 ++++++++++ .../error_groups_comparison_statistics.ts | 110 +++++++++ .../error_groups_primary_statistics.ts | 115 +++++++++ 13 files changed, 774 insertions(+), 389 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_primary_statistics.ts delete mode 100644 x-pack/test/apm_api_integration/tests/service_overview/error_groups.ts create mode 100644 x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap create mode 100644 x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts create mode 100644 x-pack/test/apm_api_integration/tests/services/error_groups_primary_statistics.ts diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index 999718e754c61..f6ffec46f9f51 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -82,9 +82,8 @@ describe('ServiceOverview', () => { /* eslint-disable @typescript-eslint/naming-convention */ const calls = { - 'GET /api/apm/services/{serviceName}/error_groups': { + 'GET /api/apm/services/{serviceName}/error_groups/primary_statistics': { error_groups: [], - total_error_groups: 0, }, 'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics': { transactionGroups: [], diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx new file mode 100644 index 0000000000000..94913c1678d21 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx @@ -0,0 +1,94 @@ +/* + * Copyright 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 { EuiBasicTableColumn } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { asInteger } from '../../../../../common/utils/formatters'; +import { px, unit } from '../../../../style/variables'; +import { SparkPlot } from '../../../shared/charts/spark_plot'; +import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; +import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; + +type ErrorGroupPrimaryStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/primary_statistics'>; +type ErrorGroupComparisonStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics'>; + +export function getColumns({ + serviceName, + errorGroupComparisonStatistics, +}: { + serviceName: string; + errorGroupComparisonStatistics: ErrorGroupComparisonStatistics; +}): Array> { + return [ + { + field: 'name', + name: i18n.translate('xpack.apm.serviceOverview.errorsTableColumnName', { + defaultMessage: 'Name', + }), + render: (_, { name, group_id: errorGroupId }) => { + return ( + + {name} + + } + /> + ); + }, + }, + { + field: 'last_seen', + name: i18n.translate( + 'xpack.apm.serviceOverview.errorsTableColumnLastSeen', + { + defaultMessage: 'Last seen', + } + ), + render: (_, { last_seen: lastSeen }) => { + return ; + }, + width: px(unit * 9), + }, + { + field: 'occurrences', + name: i18n.translate( + 'xpack.apm.serviceOverview.errorsTableColumnOccurrences', + { + defaultMessage: 'Occurrences', + } + ), + width: px(unit * 12), + render: (_, { occurrences, group_id: errorGroupId }) => { + const timeseries = + errorGroupComparisonStatistics?.[errorGroupId]?.timeseries; + return ( + + ); + }, + }, + ]; +} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx index f7f5db32e986c..109bf0483f2b0 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -7,40 +7,26 @@ import { EuiBasicTable, - EuiBasicTableColumn, EuiFlexGroup, EuiFlexItem, EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { orderBy } from 'lodash'; import React, { useState } from 'react'; -import { asInteger } from '../../../../../common/utils/formatters'; +import uuid from 'uuid'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; -import { px, unit } from '../../../../style/variables'; -import { SparkPlot } from '../../../shared/charts/spark_plot'; -import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; -import { TimestampTooltip } from '../../../shared/TimestampTooltip'; -import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; import { ServiceOverviewTableContainer } from '../service_overview_table_container'; +import { getColumns } from './get_column'; interface Props { serviceName: string; } -interface ErrorGroupItem { - name: string; - last_seen: number; - group_id: string; - occurrences: { - value: number; - timeseries: Array<{ x: number; y: number }> | null; - }; -} - type SortDirection = 'asc' | 'desc'; type SortField = 'name' | 'last_seen' | 'occurrences'; @@ -50,6 +36,11 @@ const DEFAULT_SORT = { field: 'occurrences' as const, }; +const INITIAL_STATE = { + items: [], + requestId: undefined, +}; + export function ServiceOverviewErrorsTable({ serviceName }: Props) { const { urlParams: { environment, start, end }, @@ -67,88 +58,16 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { sort: DEFAULT_SORT, }); - const columns: Array> = [ - { - field: 'name', - name: i18n.translate('xpack.apm.serviceOverview.errorsTableColumnName', { - defaultMessage: 'Name', - }), - render: (_, { name, group_id: errorGroupId }) => { - return ( - - {name} - - } - /> - ); - }, - }, - { - field: 'last_seen', - name: i18n.translate( - 'xpack.apm.serviceOverview.errorsTableColumnLastSeen', - { - defaultMessage: 'Last seen', - } - ), - render: (_, { last_seen: lastSeen }) => { - return ; - }, - width: px(unit * 9), - }, - { - field: 'occurrences', - name: i18n.translate( - 'xpack.apm.serviceOverview.errorsTableColumnOccurrences', - { - defaultMessage: 'Occurrences', - } - ), - width: px(unit * 12), - render: (_, { occurrences }) => { - return ( - - ); - }, - }, - ]; + const { pageIndex, sort } = tableOptions; - const { - data = { - totalItemCount: 0, - items: [], - tableOptions: { - pageIndex: 0, - sort: DEFAULT_SORT, - }, - }, - status, - } = useFetcher( + const { data = INITIAL_STATE, status } = useFetcher( (callApmApi) => { if (!start || !end || !transactionType) { return; } - return callApmApi({ - endpoint: 'GET /api/apm/services/{serviceName}/error_groups', + endpoint: + 'GET /api/apm/services/{serviceName}/error_groups/primary_statistics', params: { path: { serviceName }, query: { @@ -156,46 +75,68 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { start, end, uiFilters: JSON.stringify(uiFilters), - size: PAGE_SIZE, - numBuckets: 20, - pageIndex: tableOptions.pageIndex, - sortField: tableOptions.sort.field, - sortDirection: tableOptions.sort.direction, transactionType, }, }, }).then((response) => { return { + requestId: uuid(), items: response.error_groups, - totalItemCount: response.total_error_groups, - tableOptions: { - pageIndex: tableOptions.pageIndex, - sort: { - field: tableOptions.sort.field, - direction: tableOptions.sort.direction, - }, - }, }; }); }, - [ - environment, - start, - end, - serviceName, - uiFilters, - tableOptions.pageIndex, - tableOptions.sort.field, - tableOptions.sort.direction, - transactionType, - ] + [environment, start, end, serviceName, uiFilters, transactionType] ); - const { + const { requestId, items } = data; + const currentPageErrorGroups = orderBy( items, - totalItemCount, - tableOptions: { pageIndex, sort }, - } = data; + sort.field, + sort.direction + ).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE); + + const groupIds = JSON.stringify( + currentPageErrorGroups.map(({ group_id: groupId }) => groupId).sort() + ); + const { + data: errorGroupComparisonStatistics, + status: errorGroupComparisonStatisticsStatus, + } = useFetcher( + (callApmApi) => { + if ( + requestId && + currentPageErrorGroups.length && + start && + end && + transactionType + ) { + return callApmApi({ + endpoint: + 'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics', + params: { + path: { serviceName }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + numBuckets: 20, + transactionType, + groupIds, + }, + }, + }); + } + }, + // only fetches agg results when requestId or group ids change + // eslint-disable-next-line react-hooks/exhaustive-deps + [requestId, groupIds], + { preservePreviousData: false } + ); + + const columns = getColumns({ + serviceName, + errorGroupComparisonStatistics: errorGroupComparisonStatistics ?? {}, + }); return ( @@ -228,15 +169,18 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { > diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts new file mode 100644 index 0000000000000..3655fa513dfb4 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { keyBy } from 'lodash'; +import { + ERROR_GROUP_ID, + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; +import { withApmSpan } from '../../../utils/with_apm_span'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; + +export async function getServiceErrorGroupComparisonStatistics({ + serviceName, + setup, + numBuckets, + transactionType, + groupIds, + environment, +}: { + serviceName: string; + setup: Setup & SetupTimeRange; + numBuckets: number; + transactionType: string; + groupIds: string[]; + environment?: string; +}) { + return withApmSpan( + 'get_service_error_group_comparison_statistics', + async () => { + const { apmEventClient, start, end, esFilter } = setup; + + const { intervalString } = getBucketSize({ start, end, numBuckets }); + + const timeseriesResponse = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { terms: { [ERROR_GROUP_ID]: groupIds } }, + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...esFilter, + ], + }, + }, + aggs: { + error_groups: { + terms: { + field: ERROR_GROUP_ID, + size: 500, + }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { + min: start, + max: end, + }, + }, + }, + }, + }, + }, + }, + }); + + if (!timeseriesResponse.aggregations) { + return {}; + } + + const groups = timeseriesResponse.aggregations.error_groups.buckets.map( + (bucket) => { + const groupId = bucket.key as string; + return { + groupId, + timeseries: bucket.timeseries.buckets.map((timeseriesBucket) => { + return { + x: timeseriesBucket.key, + y: timeseriesBucket.doc_count, + }; + }), + }; + } + ); + + return keyBy(groups, 'groupId'); + } + ); +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_primary_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_primary_statistics.ts new file mode 100644 index 0000000000000..e6c1c5db8f2ca --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_primary_statistics.ts @@ -0,0 +1,96 @@ +/* + * Copyright 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 { + ERROR_EXC_MESSAGE, + ERROR_GROUP_ID, + ERROR_LOG_MESSAGE, + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; +import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; +import { withApmSpan } from '../../../utils/with_apm_span'; +import { getErrorName } from '../../helpers/get_error_name'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; + +export function getServiceErrorGroupPrimaryStatistics({ + serviceName, + setup, + transactionType, + environment, +}: { + serviceName: string; + setup: Setup & SetupTimeRange; + transactionType: string; + environment?: string; +}) { + return withApmSpan('get_service_error_group_primary_statistics', async () => { + const { apmEventClient, start, end, esFilter } = setup; + + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...esFilter, + ], + }, + }, + aggs: { + error_groups: { + terms: { + field: ERROR_GROUP_ID, + size: 500, + order: { + _count: 'desc', + }, + }, + aggs: { + sample: { + top_hits: { + size: 1, + _source: [ERROR_LOG_MESSAGE, ERROR_EXC_MESSAGE, '@timestamp'], + sort: { + '@timestamp': 'desc', + }, + }, + }, + }, + }, + }, + }, + }); + + const errorGroups = + response.aggregations?.error_groups.buckets.map((bucket) => ({ + group_id: bucket.key as string, + name: + getErrorName(bucket.sample.hits.hits[0]._source) ?? + NOT_AVAILABLE_LABEL, + last_seen: new Date( + bucket.sample.hits.hits[0]?._source['@timestamp'] + ).getTime(), + occurrences: bucket.doc_count, + })) ?? []; + + return { + is_aggregation_accurate: + (response.aggregations?.error_groups.sum_other_doc_count ?? 0) === 0, + error_groups: errorGroups, + }; + }); +} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 822a45fca269f..c96e02f6c1821 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -24,7 +24,8 @@ import { serviceNodeMetadataRoute, serviceAnnotationsRoute, serviceAnnotationsCreateRoute, - serviceErrorGroupsRoute, + serviceErrorGroupsPrimaryStatisticsRoute, + serviceErrorGroupsComparisonStatisticsRoute, serviceThroughputRoute, serviceDependenciesRoute, serviceMetadataDetailsRoute, @@ -126,12 +127,13 @@ const createApmApi = () => { .add(serviceNodeMetadataRoute) .add(serviceAnnotationsRoute) .add(serviceAnnotationsCreateRoute) - .add(serviceErrorGroupsRoute) + .add(serviceErrorGroupsPrimaryStatisticsRoute) .add(serviceThroughputRoute) .add(serviceDependenciesRoute) .add(serviceMetadataDetailsRoute) .add(serviceMetadataIconsRoute) .add(serviceInstancesRoute) + .add(serviceErrorGroupsComparisonStatisticsRoute) // Agent configuration .add(getSingleAgentConfigurationRoute) diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 24c7c6e3e23d7..2ce41f3d1e1a0 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -16,15 +16,17 @@ import { getServiceAnnotations } from '../lib/services/annotations'; import { getServices } from '../lib/services/get_services'; import { getServiceAgentName } from '../lib/services/get_service_agent_name'; import { getServiceDependencies } from '../lib/services/get_service_dependencies'; -import { getServiceErrorGroups } from '../lib/services/get_service_error_groups'; +import { getServiceErrorGroupPrimaryStatistics } from '../lib/services/get_service_error_groups/get_service_error_group_primary_statistics'; +import { getServiceErrorGroupComparisonStatistics } from '../lib/services/get_service_error_groups/get_service_error_group_comparison_statistics'; import { getServiceInstances } from '../lib/services/get_service_instances'; import { getServiceMetadataDetails } from '../lib/services/get_service_metadata_details'; import { getServiceMetadataIcons } from '../lib/services/get_service_metadata_icons'; import { getServiceNodeMetadata } from '../lib/services/get_service_node_metadata'; import { getServiceTransactionTypes } from '../lib/services/get_service_transaction_types'; import { getThroughput } from '../lib/services/get_throughput'; -import { offsetPreviousPeriodCoordinates } from '../utils/offset_previous_period_coordinate'; import { createRoute } from './create_route'; +import { offsetPreviousPeriodCoordinates } from '../utils/offset_previous_period_coordinate'; +import { jsonRt } from '../../common/runtime_types/json_rt'; import { comparisonRangeRt, environmentRt, @@ -276,8 +278,42 @@ export const serviceAnnotationsCreateRoute = createRoute({ }, }); -export const serviceErrorGroupsRoute = createRoute({ - endpoint: 'GET /api/apm/services/{serviceName}/error_groups', +export const serviceErrorGroupsPrimaryStatisticsRoute = createRoute({ + endpoint: + 'GET /api/apm/services/{serviceName}/error_groups/primary_statistics', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + query: t.intersection([ + environmentRt, + rangeRt, + uiFiltersRt, + t.type({ + transactionType: t.string, + }), + ]), + }), + options: { tags: ['access:apm'] }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const { + path: { serviceName }, + query: { transactionType, environment }, + } = context.params; + return getServiceErrorGroupPrimaryStatistics({ + serviceName, + setup, + transactionType, + environment, + }); + }, +}); + +export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({ + endpoint: + 'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics', params: t.type({ path: t.type({ serviceName: t.string, @@ -287,16 +323,9 @@ export const serviceErrorGroupsRoute = createRoute({ rangeRt, uiFiltersRt, t.type({ - size: toNumberRt, numBuckets: toNumberRt, - pageIndex: toNumberRt, - sortDirection: t.union([t.literal('asc'), t.literal('desc')]), - sortField: t.union([ - t.literal('last_seen'), - t.literal('occurrences'), - t.literal('name'), - ]), transactionType: t.string, + groupIds: jsonRt.pipe(t.array(t.string)), }), ]), }), @@ -306,27 +335,16 @@ export const serviceErrorGroupsRoute = createRoute({ const { path: { serviceName }, - query: { - environment, - numBuckets, - pageIndex, - size, - sortDirection, - sortField, - transactionType, - }, + query: { environment, numBuckets, transactionType, groupIds }, } = context.params; - return getServiceErrorGroups({ + return getServiceErrorGroupComparisonStatistics({ environment, serviceName, setup, - size, numBuckets, - pageIndex, - sortDirection, - sortField, transactionType, + groupIds, }); }, }); diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts index 5a4be216a817c..960cc7f526424 100644 --- a/x-pack/plugins/apm/server/routes/transactions.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -117,7 +117,7 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({ rangeRt, uiFiltersRt, t.type({ - transactionNames: jsonRt, + transactionNames: jsonRt.pipe(t.array(t.string)), numBuckets: toNumberRt, transactionType: t.string, latencyAggregationType: latencyAggregationTypeRt, diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 72ca22ae749ca..3884b3ae750a5 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -34,7 +34,6 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./service_maps/service_maps')); loadTestFile(require.resolve('./service_overview/dependencies')); - loadTestFile(require.resolve('./service_overview/error_groups')); loadTestFile(require.resolve('./service_overview/instances')); loadTestFile(require.resolve('./services/agent_name')); @@ -44,6 +43,8 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./services/throughput')); loadTestFile(require.resolve('./services/top_services')); loadTestFile(require.resolve('./services/transaction_types')); + loadTestFile(require.resolve('./services/error_groups_primary_statistics')); + loadTestFile(require.resolve('./services/error_groups_comparison_statistics')); loadTestFile(require.resolve('./settings/anomaly_detection/basic')); loadTestFile(require.resolve('./settings/anomaly_detection/no_access_user')); diff --git a/x-pack/test/apm_api_integration/tests/service_overview/error_groups.ts b/x-pack/test/apm_api_integration/tests/service_overview/error_groups.ts deleted file mode 100644 index fb7376a77382f..0000000000000 --- a/x-pack/test/apm_api_integration/tests/service_overview/error_groups.ts +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import qs from 'querystring'; -import { pick, uniqBy } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import archives from '../../common/fixtures/es_archiver/archives_metadata'; -import { registry } from '../../common/registry'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - - const archiveName = 'apm_8.0.0'; - const { start, end } = archives[archiveName]; - - registry.when( - 'Service overview error groups when data is not loaded', - { config: 'basic', archives: [] }, - () => { - it('handles the empty state', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - expect(response.status).to.be(200); - expect(response.body).to.eql({ - total_error_groups: 0, - error_groups: [], - is_aggregation_accurate: true, - }); - }); - } - ); - - registry.when( - 'Service overview error groups when data is loaded', - { config: 'basic', archives: [archiveName] }, - () => { - it('returns the correct data', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - expect(response.status).to.be(200); - - expectSnapshot(response.body.total_error_groups).toMatchInline(`5`); - - expectSnapshot(response.body.error_groups.map((group: any) => group.name)).toMatchInline(` - Array [ - "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy132[\\"revenue\\"])", - "java.io.IOException: Connection reset by peer", - "java.io.IOException: Connection reset by peer", - "Could not write JSON: Unable to find co.elastic.apm.opbeans.model.Customer with id 7173; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Unable to find co.elastic.apm.opbeans.model.Customer with id 7173 (through reference chain: co.elastic.apm.opbeans.model.Customer_$$_jvst101_3[\\"email\\"])", - "Request method 'POST' not supported", - ] - `); - - expectSnapshot(response.body.error_groups.map((group: any) => group.occurrences.value)) - .toMatchInline(` - Array [ - 5, - 3, - 2, - 1, - 1, - ] - `); - - const firstItem = response.body.error_groups[0]; - - expectSnapshot(pick(firstItem, 'group_id', 'last_seen', 'name', 'occurrences.value')) - .toMatchInline(` - Object { - "group_id": "051f95eabf120ebe2f8b0399fe3e54c5", - "last_seen": 1607437366098, - "name": "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy132[\\"revenue\\"])", - "occurrences": Object { - "value": 5, - }, - } - `); - - const visibleDataPoints = firstItem.occurrences.timeseries.filter(({ y }: any) => y > 0); - expectSnapshot(visibleDataPoints.length).toMatchInline(`4`); - }); - - it('sorts items in the correct order', async () => { - const descendingResponse = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - expect(descendingResponse.status).to.be(200); - - const descendingOccurrences = descendingResponse.body.error_groups.map( - (item: any) => item.occurrences.value - ); - - expect(descendingOccurrences).to.eql(descendingOccurrences.concat().sort().reverse()); - - const ascendingResponse = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - const ascendingOccurrences = ascendingResponse.body.error_groups.map( - (item: any) => item.occurrences.value - ); - - expect(ascendingOccurrences).to.eql(ascendingOccurrences.concat().sort().reverse()); - }); - - it('sorts items by the correct field', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size: 5, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'last_seen', - transactionType: 'request', - })}` - ); - - expect(response.status).to.be(200); - - const dates = response.body.error_groups.map((group: any) => group.last_seen); - - expect(dates).to.eql(dates.concat().sort().reverse()); - }); - - it('paginates through the items', async () => { - const size = 1; - - const firstPage = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size, - numBuckets: 20, - pageIndex: 0, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - expect(firstPage.status).to.eql(200); - - const totalItems = firstPage.body.total_error_groups; - - const pages = Math.floor(totalItems / size); - - const items = await new Array(pages) - .fill(undefined) - .reduce(async (prevItemsPromise, _, pageIndex) => { - const prevItems = await prevItemsPromise; - - const thisPage = await supertest.get( - `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ - start, - end, - uiFilters: '{}', - size, - numBuckets: 20, - pageIndex, - sortDirection: 'desc', - sortField: 'occurrences', - transactionType: 'request', - })}` - ); - - return prevItems.concat(thisPage.body.error_groups); - }, Promise.resolve([])); - - expect(items.length).to.eql(totalItems); - - expect(uniqBy(items, 'group_id').length).to.eql(totalItems); - }); - } - ); -} diff --git a/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap b/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap new file mode 100644 index 0000000000000..a536a6de67ff3 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap @@ -0,0 +1,133 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`APM API tests basic apm_8.0.0 Error groups comparison statistics when data is loaded returns the correct data 1`] = ` +Object { + "groupId": "051f95eabf120ebe2f8b0399fe3e54c5", + "timeseries": Array [ + Object { + "x": 1607435820000, + "y": 0, + }, + Object { + "x": 1607435880000, + "y": 0, + }, + Object { + "x": 1607435940000, + "y": 0, + }, + Object { + "x": 1607436000000, + "y": 0, + }, + Object { + "x": 1607436060000, + "y": 0, + }, + Object { + "x": 1607436120000, + "y": 0, + }, + Object { + "x": 1607436180000, + "y": 0, + }, + Object { + "x": 1607436240000, + "y": 0, + }, + Object { + "x": 1607436300000, + "y": 1, + }, + Object { + "x": 1607436360000, + "y": 0, + }, + Object { + "x": 1607436420000, + "y": 0, + }, + Object { + "x": 1607436480000, + "y": 0, + }, + Object { + "x": 1607436540000, + "y": 0, + }, + Object { + "x": 1607436600000, + "y": 1, + }, + Object { + "x": 1607436660000, + "y": 0, + }, + Object { + "x": 1607436720000, + "y": 0, + }, + Object { + "x": 1607436780000, + "y": 0, + }, + Object { + "x": 1607436840000, + "y": 0, + }, + Object { + "x": 1607436900000, + "y": 0, + }, + Object { + "x": 1607436960000, + "y": 0, + }, + Object { + "x": 1607437020000, + "y": 0, + }, + Object { + "x": 1607437080000, + "y": 0, + }, + Object { + "x": 1607437140000, + "y": 0, + }, + Object { + "x": 1607437200000, + "y": 2, + }, + Object { + "x": 1607437260000, + "y": 0, + }, + Object { + "x": 1607437320000, + "y": 1, + }, + Object { + "x": 1607437380000, + "y": 0, + }, + Object { + "x": 1607437440000, + "y": 0, + }, + Object { + "x": 1607437500000, + "y": 0, + }, + Object { + "x": 1607437560000, + "y": 0, + }, + Object { + "x": 1607437620000, + "y": 0, + }, + ], +} +`; diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts b/x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts new file mode 100644 index 0000000000000..a13a76e2ddb46 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts @@ -0,0 +1,110 @@ +/* + * Copyright 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 url from 'url'; +import expect from '@kbn/expect'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; + +type ErrorGroupsComparisonStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics'>; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + const { start, end } = metadata; + const groupIds = [ + '051f95eabf120ebe2f8b0399fe3e54c5', + '3bb34b98031a19c277bf59c3db82d3f3', + 'b1c3ff13ec52de11187facf9c6a82538', + '9581687a53eac06aba50ba17cbd959c5', + '97c2eef51fec10d177ade955670a2f15', + ]; + + registry.when( + 'Error groups comparison statistics when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles empty state', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/comparison_statistics`, + query: { + start, + end, + uiFilters: '{}', + numBuckets: 20, + transactionType: 'request', + groupIds: JSON.stringify(groupIds), + }, + }) + ); + expect(response.status).to.be(200); + expect(response.body).to.empty(); + }); + } + ); + + registry.when( + 'Error groups comparison statistics when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { + it('returns the correct data', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/comparison_statistics`, + query: { + start, + end, + uiFilters: '{}', + numBuckets: 20, + transactionType: 'request', + groupIds: JSON.stringify(groupIds), + }, + }) + ); + + expect(response.status).to.be(200); + + const errorGroupsComparisonStatistics = response.body as ErrorGroupsComparisonStatistics; + expect(Object.keys(errorGroupsComparisonStatistics).sort()).to.eql(groupIds.sort()); + + groupIds.forEach((groupId) => { + expect(errorGroupsComparisonStatistics[groupId]).not.to.be.empty(); + }); + + const errorgroupsComparisonStatistics = errorGroupsComparisonStatistics[groupIds[0]]; + expect( + errorgroupsComparisonStatistics.timeseries.map(({ y }) => isFinite(y)).length + ).to.be.greaterThan(0); + expectSnapshot(errorgroupsComparisonStatistics).toMatch(); + }); + + it('returns an empty list when requested groupIds are not available in the given time range', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/comparison_statistics`, + query: { + start, + end, + uiFilters: '{}', + numBuckets: 20, + transactionType: 'request', + groupIds: JSON.stringify(['foo']), + }, + }) + ); + + expect(response.status).to.be(200); + expect(response.body).to.empty(); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups_primary_statistics.ts b/x-pack/test/apm_api_integration/tests/services/error_groups_primary_statistics.ts new file mode 100644 index 0000000000000..8a334ca567f0e --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/services/error_groups_primary_statistics.ts @@ -0,0 +1,115 @@ +/* + * Copyright 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 url from 'url'; +import expect from '@kbn/expect'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; + +type ErrorGroupsPrimaryStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/primary_statistics'>; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + const { start, end } = metadata; + + registry.when( + 'Error groups primary statistics when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles empty state', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/primary_statistics`, + query: { + start, + end, + uiFilters: '{}', + transactionType: 'request', + }, + }) + ); + + expect(response.status).to.be(200); + + expect(response.status).to.be(200); + expect(response.body.error_groups).to.empty(); + expect(response.body.is_aggregation_accurate).to.eql(true); + }); + } + ); + + registry.when( + 'Error groups primary statistics when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { + it('returns the correct data', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/primary_statistics`, + query: { + start, + end, + uiFilters: '{}', + transactionType: 'request', + environment: 'production', + }, + }) + ); + + expect(response.status).to.be(200); + + const errorGroupPrimaryStatistics = response.body as ErrorGroupsPrimaryStatistics; + + expect(errorGroupPrimaryStatistics.is_aggregation_accurate).to.eql(true); + expect(errorGroupPrimaryStatistics.error_groups.length).to.be.greaterThan(0); + + expectSnapshot(errorGroupPrimaryStatistics.error_groups.map(({ name }) => name)) + .toMatchInline(` + Array [ + "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy132[\\"revenue\\"])", + "java.io.IOException: Connection reset by peer", + "java.io.IOException: Connection reset by peer", + "Could not write JSON: Unable to find co.elastic.apm.opbeans.model.Customer with id 7173; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Unable to find co.elastic.apm.opbeans.model.Customer with id 7173 (through reference chain: co.elastic.apm.opbeans.model.Customer_$$_jvst101_3[\\"email\\"])", + "Request method 'POST' not supported", + ] + `); + + const occurences = errorGroupPrimaryStatistics.error_groups.map( + ({ occurrences }) => occurrences + ); + + occurences.forEach((occurence) => expect(occurence).to.be.greaterThan(0)); + + expectSnapshot(occurences).toMatchInline(` + Array [ + 5, + 3, + 2, + 1, + 1, + ] + `); + + const firstItem = errorGroupPrimaryStatistics.error_groups[0]; + + expectSnapshot(firstItem).toMatchInline(` + Object { + "group_id": "051f95eabf120ebe2f8b0399fe3e54c5", + "last_seen": 1607437366098, + "name": "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy132[\\"revenue\\"])", + "occurrences": 5, + } + `); + }); + } + ); +} From 92301fe98d2681ba5c28e8857aaf73513df79dfc Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Fri, 19 Feb 2021 18:58:41 +0100 Subject: [PATCH 18/32] [ML] Fix event rate chart annotation position (#91899) --- .../event_rate_chart/event_rate_chart.tsx | 1 - .../charts/event_rate_chart/overlay_range.tsx | 31 ++++++------------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx index 93a3694ec8c21..48f586bba8f41 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx @@ -78,7 +78,6 @@ export const EventRateChart: FC = ({ = ({ - overlayKey, - eventRateChartData, - start, - end, - color, - showMarker = true, -}) => { - const maxHeight = Math.max(...eventRateChartData.map((e) => e.value)); - +export const OverlayRange: FC = ({ overlayKey, start, end, color, showMarker = true }) => { return ( <> = ({ coordinates: { x0: start, x1: end, - y0: 0, - y1: maxHeight, }, }, ]} @@ -62,16 +49,16 @@ export const OverlayRange: FC = ({ opacity: 0, }, }} + markerPosition={Position.Bottom} + hideTooltips={true} marker={ showMarker ? ( - <> -
-
- -
-
{timeFormatter(start)}
+
+
+
- +
{timeFormatter(start)}
+
) : undefined } /> From 4a1134c732048c08b38b138bcff1d2a3cf1f0334 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Fri, 19 Feb 2021 18:59:26 +0100 Subject: [PATCH 19/32] [Security Solution] Adds cypress-pipe (#91550) * introducing cypress-pipe * moves cypress pipe and promise packages to dev dependencies Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 5 +++-- x-pack/plugins/security_solution/cypress/support/index.js | 1 + x-pack/plugins/security_solution/cypress/tasks/timeline.ts | 5 +++-- x-pack/plugins/security_solution/cypress/tasks/timelines.ts | 6 ++---- x-pack/plugins/security_solution/cypress/tsconfig.json | 1 + yarn.lock | 5 +++++ 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 67263f53f28a2..8c8a866e9f214 100644 --- a/package.json +++ b/package.json @@ -183,7 +183,6 @@ "content-disposition": "0.5.3", "core-js": "^3.6.5", "custom-event-polyfill": "^0.3.0", - "cypress-promise": "^1.1.0", "cytoscape": "^3.10.0", "cytoscape-dagre": "^2.2.2", "d3-array": "1.2.4", @@ -613,6 +612,8 @@ "cypress": "^6.2.1", "cypress-cucumber-preprocessor": "^2.5.2", "cypress-multi-reporters": "^1.4.0", + "cypress-pipe": "^2.0.0", + "cypress-promise": "^1.1.0", "d3": "3.5.17", "d3-cloud": "1.2.5", "d3-scale": "1.0.7", @@ -833,8 +834,8 @@ "val-loader": "^1.1.1", "vega": "^5.19.1", "vega-lite": "^4.17.0", - "vega-spec-injector": "^0.0.2", "vega-schema-url-parser": "^2.1.0", + "vega-spec-injector": "^0.0.2", "vega-tooltip": "^0.25.0", "venn.js": "0.2.20", "vinyl-fs": "^3.0.3", diff --git a/x-pack/plugins/security_solution/cypress/support/index.js b/x-pack/plugins/security_solution/cypress/support/index.js index 0b6cea1a9487b..73a9f1503a47d 100644 --- a/x-pack/plugins/security_solution/cypress/support/index.js +++ b/x-pack/plugins/security_solution/cypress/support/index.js @@ -22,6 +22,7 @@ // Import commands.js using ES2015 syntax: import './commands'; +import 'cypress-pipe'; Cypress.Cookies.defaults({ preserve: 'sid', diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index c001f1fc2bc47..ada09d9c05c08 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -177,8 +177,9 @@ export const openTimelineInspectButton = () => { }; export const openTimelineFromSettings = () => { - cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').click({ force: true }); - cy.get(OPEN_TIMELINE_ICON).click({ force: true }); + const click = ($el: Cypress.ObjectLike) => cy.wrap($el).click(); + cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').pipe(click); + cy.get(OPEN_TIMELINE_ICON).pipe(click); }; export const openTimelineTemplateFromSettings = (id: string) => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/timelines.ts b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts index 5f9448a58288b..15575f304009b 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timelines.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timelines.ts @@ -20,10 +20,8 @@ export const exportTimeline = (timelineId: string) => { }; export const openTimeline = (id: string) => { - // This temporary wait here is to reduce flakeyness until we integrate cypress-pipe. Then please let us use cypress pipe. - // Ref: https://www.cypress.io/blog/2019/01/22/when-can-the-test-click/ - // Ref: https://github.com/NicholasBoll/cypress-pipe#readme - cy.get(TIMELINE(id)).should('be.visible').wait(1500).click(); + const click = ($el: Cypress.ObjectLike) => cy.wrap($el).click(); + cy.get(TIMELINE(id)).should('be.visible').pipe(click); }; export const waitForTimelinesPanelToBeLoaded = () => { diff --git a/x-pack/plugins/security_solution/cypress/tsconfig.json b/x-pack/plugins/security_solution/cypress/tsconfig.json index d0669ccb78281..270d877a362a6 100644 --- a/x-pack/plugins/security_solution/cypress/tsconfig.json +++ b/x-pack/plugins/security_solution/cypress/tsconfig.json @@ -8,6 +8,7 @@ "tsBuildInfoFile": "../../../../build/tsbuildinfo/security_solution/cypress", "types": [ "cypress", + "cypress-pipe", "node" ], "resolveJsonModule": true, diff --git a/yarn.lock b/yarn.lock index 8a8147bd25aef..e2c6ba8d320e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11680,6 +11680,11 @@ cypress-multi-reporters@^1.4.0: debug "^4.1.1" lodash "^4.17.15" +cypress-pipe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cypress-pipe/-/cypress-pipe-2.0.0.tgz#577df7a70a8603d89a96dfe4092a605962181af8" + integrity sha512-KW9s+bz4tFLucH3rBGfjW+Q12n7S4QpUSSyxiGrgPOfoHlbYWzAGB3H26MO0VTojqf9NVvfd5Kt0MH5XMgbfyg== + cypress-promise@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/cypress-promise/-/cypress-promise-1.1.0.tgz#f2d66965945fe198431aaf692d5157cea9d47b25" From 4e2601d9c98a4cbf8b215d79ddadbf377e565cca Mon Sep 17 00:00:00 2001 From: Pete Hampton Date: Fri, 19 Feb 2021 18:10:15 +0000 Subject: [PATCH 20/32] [7.12][Telemetry] Add missing fields for security telemetry (#91920) Co-authored-by: Thiago Souza --- .../server/lib/telemetry/sender.test.ts | 16 ++++ .../server/lib/telemetry/sender.ts | 80 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index 56e2f9c7c7304..d5edd4678a9a2 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -34,6 +34,11 @@ describe('TelemetryEventsSender', () => { agent: { name: 'test', }, + rule: { + id: 'X', + name: 'Y', + ruleset: 'Z', + }, file: { size: 3, path: 'X', @@ -47,6 +52,9 @@ describe('TelemetryEventsSender', () => { malware_classification: { key1: 'X', }, + malware_signature: { + key1: 'X', + }, quarantine_result: true, quarantine_message: 'this file is bad', something_else: 'nope', @@ -70,6 +78,11 @@ describe('TelemetryEventsSender', () => { agent: { name: 'test', }, + rule: { + id: 'X', + name: 'Y', + ruleset: 'Z', + }, file: { size: 3, path: 'X', @@ -81,6 +94,9 @@ describe('TelemetryEventsSender', () => { malware_classification: { key1: 'X', }, + malware_signature: { + key1: 'X', + }, quarantine_result: true, quarantine_message: 'this file is bad', }, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts index a18604fb92a40..3ee18a84e1133 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -296,16 +296,20 @@ interface AllowlistFields { // Allow list for the data we include in the events. True means that it is deep-cloned // blindly. Object contents means that we only copy the fields that appear explicitly in // the sub-object. +/* eslint-disable @typescript-eslint/naming-convention */ const allowlistEventFields: AllowlistFields = { '@timestamp': true, agent: true, Endpoint: true, + Memory_protection: true, Ransomware: true, data_stream: true, ecs: true, elastic: true, event: true, rule: { + id: true, + name: true, ruleset: true, }, file: { @@ -320,6 +324,7 @@ const allowlistEventFields: AllowlistFields = { Ext: { code_signature: true, malware_classification: true, + malware_signature: true, quarantine_result: true, quarantine_message: true, }, @@ -335,7 +340,12 @@ const allowlistEventFields: AllowlistFields = { pid: true, uptime: true, Ext: { + architecture: true, code_signature: true, + dll: true, + token: { + integrity_level_name: true, + }, }, parent: { name: true, @@ -343,12 +353,82 @@ const allowlistEventFields: AllowlistFields = { command_line: true, hash: true, Ext: { + architecture: true, code_signature: true, + dll: true, + token: { + integrity_level_name: true, + }, }, uptime: true, pid: true, ppid: true, }, + Target: { + process: { + Ext: { + architecture: true, + code_signature: true, + dll: true, + token: { + integrity_level_name: true, + }, + }, + parent: { + process: { + Ext: { + architecture: true, + code_signature: true, + dll: true, + token: { + integrity_level_name: true, + }, + }, + }, + }, + thread: { + Ext: { + call_stack: true, + start_address: true, + start_address_details: { + address_offset: true, + allocation_base: true, + allocation_protection: true, + allocation_size: true, + allocation_type: true, + base_address: true, + bytes_start_address: true, + compressed_bytes: true, + dest_bytes: true, + dest_bytes_disasm: true, + dest_bytes_disasm_hash: true, + pe: { + Ext: { + legal_copyright: true, + product_version: true, + code_signature: { + status: true, + subject_name: true, + trusted: true, + }, + company: true, + description: true, + file_version: true, + imphash: true, + original_file_name: true, + product: true, + }, + }, + pe_detected: true, + region_protection: true, + region_size: true, + region_state: true, + strings: true, + }, + }, + }, + }, + }, token: { integrity_level_name: true, }, From fb2f6abed20c32a7f45b5c804aacba3208ea15bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 19 Feb 2021 18:59:03 +0000 Subject: [PATCH 21/32] Add `@kbn/analytics` to UI Shared Deps (#91810) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-analytics/src/index.ts | 13 +++++++++---- packages/kbn-analytics/src/metrics/index.ts | 14 +++++++++----- packages/kbn-ui-shared-deps/entry.js | 1 + packages/kbn-ui-shared-deps/index.js | 1 + 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/kbn-analytics/src/index.ts b/packages/kbn-analytics/src/index.ts index 3f6dfdf6a401a..a546fb9d97e42 100644 --- a/packages/kbn-analytics/src/index.ts +++ b/packages/kbn-analytics/src/index.ts @@ -6,8 +6,13 @@ * Side Public License, v 1. */ -export { ReportHTTP, Reporter, ReporterConfig } from './reporter'; -export { UiCounterMetricType, METRIC_TYPE } from './metrics'; -export { Report, ReportManager } from './report'; +// Export types separately to the actual run-time objects +export type { ReportHTTP, ReporterConfig } from './reporter'; +export type { UiCounterMetricType } from './metrics'; +export type { Report } from './report'; +export type { Storage } from './storage'; + +export { Reporter } from './reporter'; +export { METRIC_TYPE } from './metrics'; +export { ReportManager } from './report'; export { ApplicationUsageTracker } from './application_usage_tracker'; -export { Storage } from './storage'; diff --git a/packages/kbn-analytics/src/metrics/index.ts b/packages/kbn-analytics/src/metrics/index.ts index dc03545a5ff3c..aacc3b398a16c 100644 --- a/packages/kbn-analytics/src/metrics/index.ts +++ b/packages/kbn-analytics/src/metrics/index.ts @@ -6,13 +6,17 @@ * Side Public License, v 1. */ -import { UiCounterMetric } from './ui_counter'; -import { UserAgentMetric } from './user_agent'; -import { ApplicationUsageMetric } from './application_usage'; +import type { UiCounterMetric } from './ui_counter'; +import type { UserAgentMetric } from './user_agent'; +import type { ApplicationUsageMetric } from './application_usage'; -export { UiCounterMetric, createUiCounterMetric, UiCounterMetricType } from './ui_counter'; +// Export types separately to the actual run-time objects +export type { ApplicationUsageMetric } from './application_usage'; +export type { UiCounterMetric, UiCounterMetricType } from './ui_counter'; + +export { createUiCounterMetric } from './ui_counter'; export { trackUsageAgent } from './user_agent'; -export { createApplicationUsageMetric, ApplicationUsageMetric } from './application_usage'; +export { createApplicationUsageMetric } from './application_usage'; export type Metric = UiCounterMetric | UserAgentMetric | ApplicationUsageMetric; export enum METRIC_TYPE { diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js index c02489afe7bc2..ede617908fd3d 100644 --- a/packages/kbn-ui-shared-deps/entry.js +++ b/packages/kbn-ui-shared-deps/entry.js @@ -46,3 +46,4 @@ export const LodashFp = require('lodash/fp'); // runtime deps which don't need to be copied across all bundles export const TsLib = require('tslib'); +export const KbnAnalytics = require('@kbn/analytics'); diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js index 79b4bde787851..d1217dd8db0d4 100644 --- a/packages/kbn-ui-shared-deps/index.js +++ b/packages/kbn-ui-shared-deps/index.js @@ -57,5 +57,6 @@ exports.externals = { * runtime deps which don't need to be copied across all bundles */ tslib: '__kbnSharedDeps__.TsLib', + '@kbn/analytics': '__kbnSharedDeps__.KbnAnalytics', }; exports.publicPathLoader = require.resolve('./public_path_loader'); From fc77586b5ae0e0abe97cecb3824e065fed47073c Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 19 Feb 2021 12:41:55 -0700 Subject: [PATCH 22/32] [Security Solution] Clearing up all jest errors and warnings (#91740) --- .../markdown_editor/plugins/index.ts | 7 +++-- .../components/query_bar/index.test.tsx | 31 ++++++++++++------- .../all/exceptions/exceptions_table.test.tsx | 8 +++++ .../pages/endpoint_hosts/view/index.test.tsx | 10 ++++++ .../policy/view/policy_forms/locked_card.tsx | 24 +++++++++----- .../components/flyout/header/index.test.tsx | 8 +++++ .../components/open_timeline/helpers.test.ts | 11 +++---- .../components/side_panel/index.test.tsx | 8 +++++ .../components/timeline/index.test.tsx | 8 +++++ 9 files changed, 87 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts index 7564c246513de..bc0da84133e68 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts @@ -6,13 +6,16 @@ */ import { + EuiMarkdownEditorUiPlugin, getDefaultEuiMarkdownParsingPlugins, getDefaultEuiMarkdownProcessingPlugins, + getDefaultEuiMarkdownUiPlugins, } from '@elastic/eui'; import * as timelineMarkdownPlugin from './timeline'; - -export const uiPlugins = [timelineMarkdownPlugin.plugin]; +const uiPlugins: EuiMarkdownEditorUiPlugin[] = getDefaultEuiMarkdownUiPlugins(); +uiPlugins.push(timelineMarkdownPlugin.plugin); +export { uiPlugins }; export const parsingPlugins = getDefaultEuiMarkdownParsingPlugins(); export const processingPlugins = getDefaultEuiMarkdownProcessingPlugins(); diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx index fbf56bd235789..1e998f9798e97 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx @@ -34,7 +34,16 @@ describe('QueryBar ', () => { await waitFor(() => getByTestId('queryInput')); // check for presence of query input return mount(Component); }; + let abortSpy: jest.SpyInstance; + beforeAll(() => { + const mockAbort = new AbortController(); + mockAbort.abort(); + abortSpy = jest.spyOn(window, 'AbortController').mockImplementation(() => mockAbort); + }); + afterAll(() => { + abortSpy.mockRestore(); + }); beforeEach(() => { mockOnChangeQuery.mockClear(); mockOnSubmitQuery.mockClear(); @@ -264,7 +273,6 @@ describe('QueryBar ', () => { const onChangedQueryRef = searchBarProps.onQueryChange; const onSubmitQueryRef = searchBarProps.onQuerySubmit; const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; - wrapper.setProps({ onSavedQuery: jest.fn() }); wrapper.update(); @@ -294,22 +302,21 @@ describe('QueryBar ', () => { onSavedQuery={mockOnSavedQuery} /> ); - await waitFor(() => { - const isSavedQueryPopoverOpen = () => - wrapper.find('EuiPopover[id="savedQueryPopover"]').prop('isOpen'); + const isSavedQueryPopoverOpen = () => + wrapper.find('EuiPopover[id="savedQueryPopover"]').prop('isOpen'); - expect(isSavedQueryPopoverOpen()).toBeFalsy(); + expect(isSavedQueryPopoverOpen()).toBeFalsy(); - wrapper - .find('button[data-test-subj="saved-query-management-popover-button"]') - .simulate('click'); + wrapper + .find('button[data-test-subj="saved-query-management-popover-button"]') + .simulate('click'); + await waitFor(() => { expect(isSavedQueryPopoverOpen()).toBeTruthy(); + }); + wrapper.find('button[data-test-subj="saved-query-management-save-button"]').simulate('click'); - wrapper - .find('button[data-test-subj="saved-query-management-save-button"]') - .simulate('click'); - + await waitFor(() => { expect(isSavedQueryPopoverOpen()).toBeFalsy(); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx index 3b87c786d0e36..88b42c506dabc 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx @@ -20,7 +20,15 @@ import { useAllExceptionLists } from './use_all_exception_lists'; jest.mock('../../../../../../common/lib/kibana'); jest.mock('./use_all_exception_lists'); jest.mock('../../../../../../shared_imports'); +jest.mock('@kbn/i18n/react', () => { + const originalModule = jest.requireActual('@kbn/i18n/react'); + const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); + return { + ...originalModule, + FormattedRelative, + }; +}); describe('ExceptionListsTable', () => { const exceptionList1 = getExceptionListSchemaMock(); const exceptionList2 = { ...getExceptionListSchemaMock(), list_id: 'not_endpoint_list', id: '2' }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 9925b35616c91..79e91fdeb813a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -54,6 +54,16 @@ describe('when on the list page', () => { let store: AppContextTestRender['store']; let coreStart: AppContextTestRender['coreStart']; let middlewareSpy: AppContextTestRender['middlewareSpy']; + let abortSpy: jest.SpyInstance; + beforeAll(() => { + const mockAbort = new AbortController(); + mockAbort.abort(); + abortSpy = jest.spyOn(window, 'AbortController').mockImplementation(() => mockAbort); + }); + + afterAll(() => { + abortSpy.mockRestore(); + }); beforeEach(() => { const mockedContext = createAppRootMockRenderer(); ({ history, store, coreStart, middlewareSpy } = mockedContext); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/locked_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/locked_card.tsx index 5c19a10307608..e9e9195b819d3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/locked_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/locked_card.tsx @@ -6,7 +6,15 @@ */ import React, { memo } from 'react'; -import { EuiCard, EuiIcon, EuiTextColor, EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + EuiCard, + EuiIcon, + EuiTextColor, + EuiLink, + EuiFlexGroup, + EuiFlexItem, + EuiText, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; @@ -42,8 +50,10 @@ export const LockedPolicyCard = memo(() => { } - description={ - + description={false} + > + +

@@ -59,7 +69,7 @@ export const LockedPolicyCard = memo(() => { @@ -73,9 +83,9 @@ export const LockedPolicyCard = memo(() => { />

- - } - /> + + + ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx index 6713be176586c..68b4f2e4a0c31 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx @@ -26,7 +26,15 @@ jest.mock('../../../containers/kpis', () => ({ })); const useKibanaMock = useKibana as jest.Mocked; jest.mock('../../../../common/lib/kibana'); +jest.mock('@kbn/i18n/react', () => { + const originalModule = jest.requireActual('@kbn/i18n/react'); + const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); + return { + ...originalModule, + FormattedRelative, + }; +}); const mockUseTimelineKpiResponse = { processCount: 1, userCount: 1, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 5ae5237421b54..e62b19ce599f6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -9,11 +9,7 @@ import { cloneDeep, getOr, omit } from 'lodash/fp'; import { Dispatch } from 'redux'; import ApolloClient from 'apollo-client'; -import { - mockTimelineResults, - mockTimelineResult, - mockTimelineModel, -} from '../../../common/mock/timeline_results'; +import { mockTimelineResults, mockTimelineResult, mockTimelineModel } from '../../../common/mock'; import { timelineDefaults } from '../../store/timeline/defaults'; import { setTimelineRangeDatePicker as dispatchSetTimelineRangeDatePicker } from '../../../common/store/inputs/actions'; import { @@ -37,7 +33,7 @@ import { formatTimelineResultToModel, } from './helpers'; import { OpenTimelineResult, DispatchUpdateTimeline } from './types'; -import { KueryFilterQueryKind } from '../../../common/store/model'; +import { KueryFilterQueryKind } from '../../../common/store'; import { Note } from '../../../common/lib/note'; import moment from 'moment'; import sinon from 'sinon'; @@ -1275,7 +1271,7 @@ describe('helpers', () => { describe('update a timeline', () => { const updateIsLoading = jest.fn(); - const updateTimeline = jest.fn(); + const updateTimeline = jest.fn().mockImplementation(() => jest.fn()); const selectedTimeline = { ...mockSelectedTimeline }; const apolloClient = { query: (jest.fn().mockResolvedValue(selectedTimeline) as unknown) as ApolloClient<{}>, @@ -1316,6 +1312,7 @@ describe('helpers', () => { args.duplicate, args.timelineType ); + expect(updateTimeline).toBeCalledWith({ timeline: { ...timeline, diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.test.tsx index 71ab7f01ddd54..15b2b33409707 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.test.tsx @@ -21,6 +21,14 @@ import { createStore, State } from '../../../common/store'; import { DetailsPanel } from './index'; import { TimelineExpandedDetail, TimelineTabs } from '../../../../common/types/timeline'; import { FlowTarget } from '../../../../common/search_strategy/security_solution/network'; +jest.mock('react-apollo', () => { + const original = jest.requireActual('react-apollo'); + return { + ...original, + // eslint-disable-next-line react/display-name + Query: () => <>, + }; +}); describe('Details Panel Component', () => { const state: State = { ...mockGlobalState }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index e7422e32805a9..ee2ce8cf8103b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -25,7 +25,15 @@ jest.mock('../../containers/index', () => ({ jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/components/url_state/normalize_time_range.ts'); +jest.mock('@kbn/i18n/react', () => { + const originalModule = jest.requireActual('@kbn/i18n/react'); + const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); + return { + ...originalModule, + FormattedRelative, + }; +}); const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); From a6a567f47658df3e8ff2fd7b0c0e27f59fcf3de1 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 19 Feb 2021 11:56:09 -0800 Subject: [PATCH 23/32] Use documentation link service for snapshot restore (#91596) Co-authored-by: Alison Goryachev --- ...-plugin-core-public.doclinksstart.links.md | 5 ++ ...kibana-plugin-core-public.doclinksstart.md | 2 +- .../public/doc_links/doc_links_service.ts | 26 ++++++ src/core/public/public.api.md | 5 ++ .../helpers/setup_environment.tsx | 4 +- .../policy_form/steps/step_logistics.tsx | 13 ++- .../policy_form/steps/step_retention.tsx | 5 +- .../steps/step_settings/step_settings.tsx | 5 +- .../components/repository_form/step_one.tsx | 11 ++- .../components/repository_form/step_two.tsx | 6 +- .../data_streams_global_state_call_out.tsx | 10 +-- .../steps/step_logistics/step_logistics.tsx | 7 +- .../steps/step_settings.tsx | 8 +- .../retention_update_modal_provider.tsx | 6 +- .../public/application/constants/index.ts | 12 --- .../public/application/lib/type_to_doc_url.ts | 31 ++++++++ .../application/mount_management_section.ts | 3 - .../public/application/sections/home/home.tsx | 6 +- .../repository_details/repository_details.tsx | 7 +- .../home/snapshot_list/snapshot_list.tsx | 8 +- .../documentation/documentation_links.ts | 79 ------------------- .../services/documentation/index.ts | 8 -- 22 files changed, 117 insertions(+), 150 deletions(-) create mode 100644 x-pack/plugins/snapshot_restore/public/application/lib/type_to_doc_url.ts delete mode 100644 x-pack/plugins/snapshot_restore/public/application/services/documentation/documentation_links.ts delete mode 100644 x-pack/plugins/snapshot_restore/public/application/services/documentation/index.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 54c065480b113..026032a7b0740 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 @@ -117,6 +117,7 @@ readonly links: { }; readonly date: { readonly dateMath: string; + readonly dateMathIndexNames: string; }; readonly management: Record; readonly ml: Record; @@ -130,6 +131,7 @@ readonly links: { createApiKey: string; createPipeline: string; createTransformRequest: string; + cronExpressions: string; executeWatchActionModes: string; indexExists: string; openIndex: string; @@ -137,6 +139,7 @@ readonly links: { painlessExecute: string; painlessExecuteAPIContexts: string; putComponentTemplateMetadata: string; + putSnapshotLifecyclePolicy: string; putWatch: string; updateTransform: string; }>; @@ -158,5 +161,7 @@ readonly links: { }>; readonly watcher: Record; readonly ccs: Record; + readonly plugins: Record; + readonly snapshotRestore: Record; }; ``` 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 0bca16a0bb710..d653623d5fe22 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 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: 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 indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putWatch: 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>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
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: 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 indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: 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<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: 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;
putWatch: 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>;
} | | diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 77792286d6839..23534b3cf9210 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -20,6 +20,7 @@ export class DocLinksService { const DOC_LINK_VERSION = injectedMetadata.getKibanaBranch(); const ELASTIC_WEBSITE_URL = 'https://www.elastic.co/'; const ELASTICSEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`; + const PLUGIN_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`; return deepFreeze({ DOC_LINK_VERSION, @@ -126,6 +127,7 @@ export class DocLinksService { addData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/connect-to-elasticsearch.html`, kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`, elasticsearch: { + indexModules: `${ELASTICSEARCH_DOCS}index-modules.html`, mapping: `${ELASTICSEARCH_DOCS}mapping.html`, remoteClusters: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html`, remoteClustersProxy: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#proxy-mode`, @@ -145,6 +147,7 @@ export class DocLinksService { }, date: { dateMath: `${ELASTICSEARCH_DOCS}common-options.html#date-math`, + dateMathIndexNames: `${ELASTICSEARCH_DOCS}date-math-index-names.html`, }, management: { kibanaSearchSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-search-settings`, @@ -239,6 +242,7 @@ export class DocLinksService { createApiKey: `${ELASTICSEARCH_DOCS}security-api-create-api-key.html`, createPipeline: `${ELASTICSEARCH_DOCS}put-pipeline-api.html`, createTransformRequest: `${ELASTICSEARCH_DOCS}put-transform.html#put-transform-request-body`, + cronExpressions: `${ELASTICSEARCH_DOCS}cron-expressions.html`, executeWatchActionModes: `${ELASTICSEARCH_DOCS}watcher-api-execute-watch.html#watcher-api-execute-watch-action-mode`, indexExists: `${ELASTICSEARCH_DOCS}indices-exists.html`, openIndex: `${ELASTICSEARCH_DOCS}indices-open-close.html`, @@ -246,9 +250,26 @@ export class DocLinksService { painlessExecute: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/painless/${DOC_LINK_VERSION}/painless-execute-api.html`, painlessExecuteAPIContexts: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/painless/${DOC_LINK_VERSION}/painless-execute-api.html#_contexts`, putComponentTemplateMetadata: `${ELASTICSEARCH_DOCS}indices-component-template.html#component-templates-metadata`, + putSnapshotLifecyclePolicy: `${ELASTICSEARCH_DOCS}slm-api-put-policy.html`, putWatch: `${ELASTICSEARCH_DOCS}/watcher-api-put-watch.html`, updateTransform: `${ELASTICSEARCH_DOCS}update-transform.html`, }, + plugins: { + azureRepo: `${PLUGIN_DOCS}repository-azure.html`, + gcsRepo: `${PLUGIN_DOCS}repository-gcs.html`, + hdfsRepo: `${PLUGIN_DOCS}repository-hdfs.html`, + s3Repo: `${PLUGIN_DOCS}repository-s3.html`, + snapshotRestoreRepos: `${PLUGIN_DOCS}repository.html`, + }, + snapshotRestore: { + guide: `${ELASTICSEARCH_DOCS}snapshot-restore.html`, + changeIndexSettings: `${ELASTICSEARCH_DOCS}snapshots-restore-snapshot.html#change-index-settings-during-restore`, + createSnapshot: `${ELASTICSEARCH_DOCS}snapshots-take-snapshot.html`, + registerSharedFileSystem: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-filesystem-repository`, + registerSourceOnly: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-source-only-repository`, + registerUrl: `${ELASTICSEARCH_DOCS}snapshots-register-repository.html#snapshots-read-only-repository`, + restoreSnapshot: `${ELASTICSEARCH_DOCS}snapshots-restore-snapshot.html`, + }, }, }); } @@ -368,6 +389,7 @@ export interface DocLinksStart { }; readonly date: { readonly dateMath: string; + readonly dateMathIndexNames: string; }; readonly management: Record; readonly ml: Record; @@ -381,6 +403,7 @@ export interface DocLinksStart { createApiKey: string; createPipeline: string; createTransformRequest: string; + cronExpressions: string; executeWatchActionModes: string; indexExists: string; openIndex: string; @@ -388,6 +411,7 @@ export interface DocLinksStart { painlessExecute: string; painlessExecuteAPIContexts: string; putComponentTemplateMetadata: string; + putSnapshotLifecyclePolicy: string; putWatch: string; updateTransform: string; }>; @@ -409,5 +433,7 @@ export interface DocLinksStart { }>; readonly watcher: Record; readonly ccs: Record; + readonly plugins: Record; + readonly snapshotRestore: Record; }; } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index e29173d1495af..b068606b88047 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -587,6 +587,7 @@ export interface DocLinksStart { }; readonly date: { readonly dateMath: string; + readonly dateMathIndexNames: string; }; readonly management: Record; readonly ml: Record; @@ -600,6 +601,7 @@ export interface DocLinksStart { createApiKey: string; createPipeline: string; createTransformRequest: string; + cronExpressions: string; executeWatchActionModes: string; indexExists: string; openIndex: string; @@ -607,6 +609,7 @@ export interface DocLinksStart { painlessExecute: string; painlessExecuteAPIContexts: string; putComponentTemplateMetadata: string; + putSnapshotLifecyclePolicy: string; putWatch: string; updateTransform: string; }>; @@ -628,6 +631,8 @@ export interface DocLinksStart { }>; readonly watcher: Record; readonly ccs: Record; + readonly plugins: Record; + readonly snapshotRestore: Record; }; } diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx index 83003962f473b..3f066875e880c 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx @@ -21,7 +21,6 @@ import { AppContextProvider } from '../../../public/application/app_context'; import { textService } from '../../../public/application/services/text'; import { init as initHttpRequests } from './http_requests'; import { UiMetricService } from '../../../public/application/services'; -import { documentationLinksService } from '../../../public/application/services/documentation'; const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); @@ -40,7 +39,7 @@ export const services = { setUiMetricService(services.uiMetricService); const appDependencies = { - core: coreMock.createSetup(), + core: coreMock.createStart(), services, config: { slm_ui: { enabled: true }, @@ -53,7 +52,6 @@ export const setupEnvironment = () => { httpService.setup(mockHttpClient); breadcrumbService.setup(() => undefined); textService.setup(i18n); - documentationLinksService.setup({} as any); docTitleService.setup(() => undefined); const { server, httpRequestsMockHelpers } = initHttpRequests(); diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx index f09812011f035..5545e8a87d99d 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx @@ -26,11 +26,10 @@ import { import { Repository } from '../../../../../common/types'; import { Frequency, CronEditor, SectionError } from '../../../../shared_imports'; -import { useServices } from '../../../app_context'; +import { useCore, useServices } from '../../../app_context'; import { DEFAULT_POLICY_SCHEDULE, DEFAULT_POLICY_FREQUENCY } from '../../../constants'; import { useLoadRepositories } from '../../../services/http'; import { linkToAddRepository } from '../../../services/navigation'; -import { documentationLinksService } from '../../../services/documentation'; import { SectionLoading } from '../../'; import { StepProps } from './'; @@ -57,6 +56,7 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ } = useLoadRepositories(); const { i18n, history } = useServices(); + const { docLinks } = useCore(); const [showRepositoryNotFoundWarning, setShowRepositoryNotFoundWarning] = useState( false @@ -338,10 +338,7 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ defaultMessage="Supports date math expressions. {docLink}" values={{ docLink: ( - + = ({ defaultMessage="Use cron expression. {docLink}" values={{ docLink: ( - + = ({ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx index 15da65443ceb8..62f38ce9952df 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx @@ -21,9 +21,9 @@ import { import { SlmPolicyPayload } from '../../../../../common/types'; import { TIME_UNITS } from '../../../../../common/constants'; -import { documentationLinksService } from '../../../services/documentation'; import { StepProps } from './'; import { textService } from '../../../services/text'; +import { useCore } from '../../../app_context'; const getExpirationTimeOptions = (unitSize = '0') => Object.entries(TIME_UNITS).map(([_key, value]) => ({ @@ -37,6 +37,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ errors, }) => { const { retention = {} } = policy; + const { docLinks } = useCore(); const updatePolicyRetention = ( updatedFields: Partial, @@ -224,7 +225,7 @@ export const PolicyStepRetention: React.FunctionComponent = ({ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx index 94b296dcf9c04..dcaad024eb0f7 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx @@ -19,10 +19,10 @@ import { } from '@elastic/eui'; import { SlmPolicyPayload } from '../../../../../../common/types'; -import { documentationLinksService } from '../../../../services/documentation'; import { StepProps } from '../'; import { IndicesAndDataStreamsField } from './fields'; +import { useCore } from '../../../../app_context'; export const PolicyStepSettings: React.FunctionComponent = ({ policy, @@ -31,6 +31,7 @@ export const PolicyStepSettings: React.FunctionComponent = ({ updatePolicy, errors, }) => { + const { docLinks } = useCore(); const { config = {}, isManagedPolicy } = policy; const updatePolicyConfig = ( @@ -184,7 +185,7 @@ export const PolicyStepSettings: React.FunctionComponent = ({ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/repository_form/step_one.tsx b/x-pack/plugins/snapshot_restore/public/application/components/repository_form/step_one.tsx index 6e072a6fac751..91802c6bcf1fa 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/repository_form/step_one.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/repository_form/step_one.tsx @@ -28,11 +28,12 @@ import { Repository, RepositoryType, EmptyRepository } from '../../../../common/ import { REPOSITORY_TYPES } from '../../../../common'; import { SectionError, Error } from '../../../shared_imports'; -import { documentationLinksService } from '../../services/documentation'; import { useLoadRepositoryTypes } from '../../services/http'; import { textService } from '../../services/text'; import { RepositoryValidation } from '../../services/validation'; import { SectionLoading, RepositoryTypeLogo } from '../'; +import { useCore } from '../../app_context'; +import { getRepositoryTypeDocUrl } from '../../lib/type_to_doc_url'; interface Props { repository: Repository | EmptyRepository; @@ -54,6 +55,8 @@ export const RepositoryFormStepOne: React.FunctionComponent = ({ data: repositoryTypes = [], } = useLoadRepositoryTypes(); + const { docLinks } = useCore(); + const hasValidationErrors: boolean = !validation.isValid; const onTypeChange = (newType: RepositoryType) => { @@ -72,7 +75,7 @@ export const RepositoryFormStepOne: React.FunctionComponent = ({ }; const pluginDocLink = ( - + = ({ description={} /* EuiCard requires `description` */ footer={ = ({ values={{ docLink: ( = ({ saveError, onBack, }) => { + const { docLinks } = useCore(); const hasValidationErrors: boolean = !validation.isValid; const { name, @@ -76,7 +78,7 @@ export const RepositoryFormStepTwo: React.FunctionComponent = ({ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx index 6e3dc0a227042..e99f122efaeeb 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx @@ -9,8 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { FunctionComponent } from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; - -import { documentationLinksService } from '../../../../services/documentation'; +import { useCore } from '../../../../app_context'; const i18nTexts = { callout: { @@ -20,13 +19,13 @@ const i18nTexts = { 'This snapshot contains {count, plural, one {a data stream} other {data streams}}', values: { count }, }), - body: () => ( + body: (docLink: string) => ( + {i18n.translate( 'xpack.snapshotRestore.restoreForm.dataStreamsWarningCallOut.body.learnMoreLink', { defaultMessage: 'Learn more' } @@ -44,6 +43,7 @@ interface Props { } export const DataStreamsGlobalStateCallOut: FunctionComponent = ({ dataStreamsCount }) => { + const { docLinks } = useCore(); return ( = ({ dataSt iconType="alert" color="warning" > - {i18nTexts.callout.body()} + {i18nTexts.callout.body(docLinks.links.snapshotRestore.createSnapshot)} ); }; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx index e88bc7feef399..bb66585579d7d 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx @@ -27,9 +27,7 @@ import { EuiSelectableOption } from '@elastic/eui'; import { csvToArray, isDataStreamBackingIndex } from '../../../../../../common/lib'; import { RestoreSettings } from '../../../../../../common/types'; -import { documentationLinksService } from '../../../../services/documentation'; - -import { useServices } from '../../../../app_context'; +import { useCore, useServices } from '../../../../app_context'; import { orderDataStreamsAndIndices } from '../../../lib'; import { DataStreamBadge } from '../../../data_stream_badge'; @@ -47,6 +45,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = errors, }) => { const { i18n } = useServices(); + const { docLinks } = useCore(); const { indices: unfilteredSnapshotIndices, dataStreams: snapshotDataStreams = [], @@ -166,7 +165,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx index 3f4789bceac59..1c27ee424ea31 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx @@ -24,8 +24,7 @@ import { } from '@elastic/eui'; import { RestoreSettings } from '../../../../../common/types'; import { REMOVE_INDEX_SETTINGS_SUGGESTIONS } from '../../../constants'; -import { documentationLinksService } from '../../../services/documentation'; -import { useServices } from '../../../app_context'; +import { useCore, useServices } from '../../../app_context'; import { StepProps } from './'; export const RestoreSnapshotStepSettings: React.FunctionComponent = ({ @@ -35,6 +34,7 @@ export const RestoreSnapshotStepSettings: React.FunctionComponent = ( errors, }) => { const { i18n } = useServices(); + const { docLinks } = useCore(); const { indexSettings, ignoreIndexSettings } = restoreSettings; const { dataStreams } = snapshotDetails; @@ -63,7 +63,7 @@ export const RestoreSnapshotStepSettings: React.FunctionComponent = ( // Index settings doc link const indexSettingsDocLink = ( - + = ( diff --git a/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx b/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx index 73e19eee8bf7a..dcf087bb9ddc8 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx @@ -23,8 +23,7 @@ import { EuiCallOut, } from '@elastic/eui'; -import { useServices, useToastNotifications } from '../app_context'; -import { documentationLinksService } from '../services/documentation'; +import { useCore, useServices, useToastNotifications } from '../app_context'; import { Frequency, CronEditor } from '../../shared_imports'; import { DEFAULT_RETENTION_SCHEDULE, DEFAULT_RETENTION_FREQUENCY } from '../constants'; import { updateRetentionSchedule } from '../services/http'; @@ -44,6 +43,7 @@ export const RetentionSettingsUpdateModalProvider: React.FunctionComponent { const { i18n } = useServices(); + const { docLinks } = useCore(); const toastNotifications = useToastNotifications(); const [retentionSchedule, setRetentionSchedule] = useState(DEFAULT_RETENTION_SCHEDULE); @@ -185,7 +185,7 @@ export const RetentionSettingsUpdateModalProvider: React.FunctionComponent + { + switch (type) { + case REPOSITORY_TYPES.fs: + return docLinks.links.snapshotRestore.registerSharedFileSystem; + case REPOSITORY_TYPES.url: + return `${docLinks.links.snapshotRestore.registerUrl}`; + case REPOSITORY_TYPES.source: + return `${docLinks.links.snapshotRestore.registerSourceOnly}`; + case REPOSITORY_TYPES.s3: + return `${docLinks.links.plugins.s3Repo}`; + case REPOSITORY_TYPES.hdfs: + return `${docLinks.links.plugins.hdfsRepo}`; + case REPOSITORY_TYPES.azure: + return `${docLinks.links.plugins.azureRepo}`; + case REPOSITORY_TYPES.gcs: + return `${docLinks.links.plugins.gcsRepo}`; + default: + return `${docLinks.links.snapshotRestore.guide}`; + } +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/mount_management_section.ts b/x-pack/plugins/snapshot_restore/public/application/mount_management_section.ts index e947dc8ee4ab6..2077e37227fb7 100644 --- a/x-pack/plugins/snapshot_restore/public/application/mount_management_section.ts +++ b/x-pack/plugins/snapshot_restore/public/application/mount_management_section.ts @@ -13,7 +13,6 @@ import { ClientConfigType } from '../types'; import { httpService } from './services/http'; import { UiMetricService } from './services'; import { breadcrumbService, docTitleService } from './services/navigation'; -import { documentationLinksService } from './services/documentation'; import { AppDependencies } from './app_context'; import { renderApp } from '.'; @@ -28,13 +27,11 @@ export async function mountManagementSection( const { element, setBreadcrumbs, history } = params; const [core] = await coreSetup.getStartServices(); const { - docLinks, chrome: { docTitle }, } = core; docTitleService.setup(docTitle.change); breadcrumbService.setup(setBreadcrumbs); - documentationLinksService.setup(docLinks); const appDependencies: AppDependencies = { core, diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx index 130488d370c13..e4a23bac636d8 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx @@ -23,14 +23,13 @@ import { } from '@elastic/eui'; import { BASE_PATH, Section } from '../../constants'; -import { useConfig } from '../../app_context'; +import { useConfig, useCore } from '../../app_context'; import { breadcrumbService, docTitleService } from '../../services/navigation'; import { RepositoryList } from './repository_list'; import { SnapshotList } from './snapshot_list'; import { RestoreList } from './restore_list'; import { PolicyList } from './policy_list'; -import { documentationLinksService } from '../../services/documentation'; interface MatchParams { section: Section; @@ -43,6 +42,7 @@ export const SnapshotRestoreHome: React.FunctionComponent { const { slm_ui: slmUi } = useConfig(); + const { docLinks } = useCore(); const tabs: Array<{ id: Section; @@ -114,7 +114,7 @@ export const SnapshotRestoreHome: React.FunctionComponent = ({ onRepositoryDeleted, }) => { const { i18n, history } = useServices(); + const { docLinks } = useCore(); const { error, data: repositoryDetails } = useLoadRepository(repositoryName); const [verification, setVerification] = useState(undefined); const [cleanup, setCleanup] = useState(undefined); @@ -223,7 +224,7 @@ export const RepositoryDetails: React.FunctionComponent = ({ @@ -270,7 +270,7 @@ export const SnapshotList: React.FunctionComponent

diff --git a/x-pack/plugins/snapshot_restore/public/application/services/documentation/documentation_links.ts b/x-pack/plugins/snapshot_restore/public/application/services/documentation/documentation_links.ts deleted file mode 100644 index 602a662d1ece2..0000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/services/documentation/documentation_links.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DocLinksStart } from '../../../../../../../src/core/public'; -import { REPOSITORY_TYPES } from '../../../../common/constants'; -import { RepositoryType } from '../../../../common/types'; -import { REPOSITORY_DOC_PATHS } from '../../constants'; - -class DocumentationLinksService { - private esDocBasePath: string = ''; - private esPluginDocBasePath: string = ''; - - public setup(docLinks: DocLinksStart): void { - const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks; - const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; - - this.esDocBasePath = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}/`; - this.esPluginDocBasePath = `${docsBase}/elasticsearch/plugins/${DOC_LINK_VERSION}/`; - } - - public getRepositoryPluginDocUrl() { - return `${this.esPluginDocBasePath}${REPOSITORY_DOC_PATHS.plugins}`; - } - - public getRepositoryTypeDocUrl(type?: RepositoryType) { - switch (type) { - case REPOSITORY_TYPES.fs: - return `${this.esDocBasePath}${REPOSITORY_DOC_PATHS.fs}`; - case REPOSITORY_TYPES.url: - return `${this.esDocBasePath}${REPOSITORY_DOC_PATHS.url}`; - case REPOSITORY_TYPES.source: - return `${this.esDocBasePath}${REPOSITORY_DOC_PATHS.source}`; - case REPOSITORY_TYPES.s3: - return `${this.esPluginDocBasePath}${REPOSITORY_DOC_PATHS.s3}`; - case REPOSITORY_TYPES.hdfs: - return `${this.esPluginDocBasePath}${REPOSITORY_DOC_PATHS.hdfs}`; - case REPOSITORY_TYPES.azure: - return `${this.esPluginDocBasePath}${REPOSITORY_DOC_PATHS.azure}`; - case REPOSITORY_TYPES.gcs: - return `${this.esPluginDocBasePath}${REPOSITORY_DOC_PATHS.gcs}`; - default: - return `${this.esDocBasePath}${REPOSITORY_DOC_PATHS.default}`; - } - } - - public getSnapshotDocUrl() { - return `${this.esDocBasePath}snapshots-take-snapshot.html`; - } - - public getRestoreDocUrl() { - return `${this.esDocBasePath}snapshots-restore-snapshot.html`; - } - - public getRestoreIndexSettingsUrl() { - return `${this.esDocBasePath}snapshots-restore-snapshot.html#_changing_index_settings_during_restore`; - } - - public getIndexSettingsUrl() { - return `${this.esDocBasePath}index-modules.html`; - } - - public getDateMathIndexNamesUrl() { - return `${this.esDocBasePath}date-math-index-names.html`; - } - - public getSlmUrl() { - return `${this.esDocBasePath}slm-api-put.html`; - } - - public getCronUrl() { - return `${this.esDocBasePath}trigger-schedule.html#schedule-cron`; - } -} - -export const documentationLinksService = new DocumentationLinksService(); diff --git a/x-pack/plugins/snapshot_restore/public/application/services/documentation/index.ts b/x-pack/plugins/snapshot_restore/public/application/services/documentation/index.ts deleted file mode 100644 index 995c63513bda1..0000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/services/documentation/index.ts +++ /dev/null @@ -1,8 +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 { documentationLinksService } from './documentation_links'; From 126ce49446b66b40f76c05aedfe01d02a740540f Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Fri, 19 Feb 2021 15:01:51 -0600 Subject: [PATCH 24/32] [docker] Default server.name to hostname (#90799) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/setup/docker.asciidoc | 1 - .../docker_generator/templates/kibana_yml.template.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index 75a9799d70fbd..25883307e69f0 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -120,7 +120,6 @@ The following settings have different default values when using the Docker images: [horizontal] -`server.name`:: `kibana` `server.host`:: `"0.0.0.0"` `elasticsearch.hosts`:: `http://elasticsearch:9200` `monitoring.ui.container.elasticsearch.enabled`:: `true` diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts index 86443061fca64..c8eb16530507f 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts @@ -17,7 +17,6 @@ function generator({ imageFlavor }: TemplateContext) { # # Default Kibana configuration for docker target - server.name: kibana server.host: "0.0.0.0" elasticsearch.hosts: [ "http://elasticsearch:9200" ] ${!imageFlavor ? 'monitoring.ui.container.elasticsearch.enabled: true' : ''} From f1db49ff70bb201670bc64b9e45f0729ef4ab071 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 19 Feb 2021 16:21:53 -0500 Subject: [PATCH 25/32] Skip flaky apm test #91673 (#92065) --- x-pack/test/apm_api_integration/tests/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 3884b3ae750a5..c030ffb347c86 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -15,7 +15,8 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte this.tags('ciGroup1'); loadTestFile(require.resolve('./alerts/chart_preview')); - loadTestFile(require.resolve('./correlations/slow_transactions')); + // Flaky, see https://github.com/elastic/kibana/issues/91673 + // loadTestFile(require.resolve('./correlations/slow_transactions')); loadTestFile(require.resolve('./csm/csm_services')); loadTestFile(require.resolve('./csm/has_rum_data')); From 5789a26ce306eab18a57797b4bb0e6b0e0f117ff Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Fri, 19 Feb 2021 17:55:29 -0500 Subject: [PATCH 26/32] [Security Solution] [Detections] do not truncate filename in value list table in modal (#91952) --- .../components/value_lists_management_modal/table_helpers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx index ab45475fa8e84..cc9ba225cac0e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table_helpers.tsx @@ -29,7 +29,7 @@ export const buildColumns = ( { field: 'name', name: i18n.COLUMN_FILE_NAME, - truncateText: true, + truncateText: false, }, { field: 'type', From 52c4deceaaf203c99c3bad63f4254e37a21a1b39 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Fri, 19 Feb 2021 17:56:16 -0500 Subject: [PATCH 27/32] [Security Solution] [Detections] add overflow-wrap for description (#91945) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../detections/components/rules/description_step/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index a4812a6372abc..4e330f7c0bd07 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -49,6 +49,7 @@ const DescriptionListContainer = styled(EuiDescriptionList)` } &.euiDescriptionList--column .euiDescriptionList__description { width: 70%; + overflow-wrap: anywhere; } `; From 087449cbf6b6284d0a610791897c4d5fac67b5c8 Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Fri, 19 Feb 2021 19:16:14 -0500 Subject: [PATCH 28/32] [Security Solution][Detections] Adds more granular validation for nested fields (#92041) --- .../types/non_empty_nested_entries_array.ts | 3 +- x-pack/plugins/lists/common/shared_exports.ts | 1 + .../common/shared_imports.ts | 1 + .../components/exceptions/helpers.test.tsx | 46 +++++++++++++++++++ .../common/components/exceptions/helpers.tsx | 34 ++++++++++---- 5 files changed, 75 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.ts index 0b5b49c2715d4..722f5dd600eec 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.ts @@ -12,7 +12,8 @@ import { entriesMatchAny } from './entry_match_any'; import { entriesMatch } from './entry_match'; import { entriesExists } from './entry_exists'; -export const nestedEntriesArray = t.array(t.union([entriesMatch, entriesMatchAny, entriesExists])); +export const nestedEntryItem = t.union([entriesMatch, entriesMatchAny, entriesExists]); +export const nestedEntriesArray = t.array(nestedEntryItem); export type NestedEntriesArray = t.TypeOf; /** diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index 6dcda5d1f8c24..4c4ee19d29bcd 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -36,6 +36,7 @@ export { listSchema, entry, entriesNested, + nestedEntryItem, entriesMatch, entriesMatchAny, entriesExists, diff --git a/x-pack/plugins/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts index 988f0ad0c125d..94afbb948bf42 100644 --- a/x-pack/plugins/security_solution/common/shared_imports.ts +++ b/x-pack/plugins/security_solution/common/shared_imports.ts @@ -35,6 +35,7 @@ export { listSchema, entry, entriesNested, + nestedEntryItem, entriesMatch, entriesMatchAny, entriesExists, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index b09ad60b239de..fdf7594a550a2 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -326,6 +326,52 @@ describe('Exception helpers', () => { expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); }); + test('it removes the "nested" entry entries with "value" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EntryNested = { + field: 'host.name', + type: OperatorTypeEnum.NESTED, + entries: [getEntryMatchMock(), { ...getEntryMatchMock(), value: '' }], + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([ + { + ...getExceptionListItemSchemaMock(), + entries: [ + ...getExceptionListItemSchemaMock().entries, + { ...mockEmptyException, entries: [getEntryMatchMock()] }, + ], + }, + ]); + }); + + test('it removes the "nested" entry item if all its entries are invalid', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EntryNested = { + field: 'host.name', + type: OperatorTypeEnum.NESTED, + entries: [{ ...getEntryMatchMock(), value: '' }], + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + test('it removes `temporaryId` from items', () => { const { meta, ...rest } = getNewExceptionItem({ listId: '123', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 507fd51a90486..13ee06e8cbac9 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -32,6 +32,7 @@ import { comment, entry, entriesNested, + nestedEntryItem, createExceptionListItemSchema, exceptionListItemSchema, UpdateExceptionListItemSchema, @@ -173,16 +174,31 @@ export const filterExceptionItems = ( ): Array => { return exceptions.reduce>( (acc, exception) => { - const entries = exception.entries.filter((t) => { - const [validatedEntry] = validate(t, entry); - const [validatedNestedEntry] = validate(t, entriesNested); + const entries = exception.entries.reduce((nestedAcc, singleEntry) => { + if (singleEntry.type === 'nested') { + const nestedEntriesArray = singleEntry.entries.filter((singleNestedEntry) => { + const [validatedNestedEntry] = validate(singleNestedEntry, nestedEntryItem); + return validatedNestedEntry != null; + }); + + const [validatedNestedEntry] = validate( + { ...singleEntry, entries: nestedEntriesArray }, + entriesNested + ); + + if (validatedNestedEntry != null) { + return [...nestedAcc, validatedNestedEntry]; + } + return nestedAcc; + } else { + const [validatedEntry] = validate(singleEntry, entry); - if (validatedEntry != null || validatedNestedEntry != null) { - return true; + if (validatedEntry != null) { + return [...nestedAcc, validatedEntry]; + } + return nestedAcc; } - - return false; - }); + }, []); const item = { ...exception, entries }; @@ -401,7 +417,7 @@ export const getCodeSignatureValue = ( return codeSignature.map((signature) => { return { subjectName: signature.subject_name ?? '', - trusted: signature.trusted ?? '', + trusted: signature.trusted.toString() ?? '', }; }); } else { From d23c01b4beb1199ae5dec254d69fd197f6f4b8b3 Mon Sep 17 00:00:00 2001 From: Michail Yasonik Date: Fri, 19 Feb 2021 19:21:20 -0500 Subject: [PATCH 29/32] Enabling Uptime and Dashboard a11y test (#91017) --- .../services/a11y/analyze_with_axe.js | 13 ++- .../services/dashboard/panel_actions.ts | 2 +- test/functional/services/toasts.ts | 15 +++- .../apps/dashboard_edit_panel.ts | 87 ++++++------------- x-pack/test/accessibility/apps/uptime.ts | 9 +- 5 files changed, 60 insertions(+), 66 deletions(-) diff --git a/test/accessibility/services/a11y/analyze_with_axe.js b/test/accessibility/services/a11y/analyze_with_axe.js index 3d1e257235f55..4bd29dbab7efc 100644 --- a/test/accessibility/services/a11y/analyze_with_axe.js +++ b/test/accessibility/services/a11y/analyze_with_axe.js @@ -31,8 +31,19 @@ export function analyzeWithAxe(context, options, callback) { selector: '[data-test-subj="comboBoxSearchInput"] *', }, { + // EUI bug: https://github.com/elastic/eui/issues/4474 id: 'aria-required-parent', - selector: '[class=*"euiDataGridRowCell"][role="gridcell"] ', + selector: '[class=*"euiDataGridRowCell"][role="gridcell"]', + }, + { + // 3rd-party library; button has aria-describedby + id: 'button-name', + selector: '[data-rbd-drag-handle-draggable-id]', + }, + { + // EUI bug: https://github.com/elastic/eui/issues/4536 + id: 'duplicate-id', + selector: '.euiSuperDatePicker *', }, ], }); diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index 041051398262e..0101d2b2a1916 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -96,7 +96,7 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft async clickExpandPanelToggle() { log.debug(`clickExpandPanelToggle`); - this.openContextMenu(); + await this.openContextMenu(); const isActionVisible = await testSubjects.exists(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); if (!isActionVisible) await this.clickContextMenuMoreItem(); await testSubjects.click(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); diff --git a/test/functional/services/toasts.ts b/test/functional/services/toasts.ts index b9db0a1ee9b7b..aeaf79e75574a 100644 --- a/test/functional/services/toasts.ts +++ b/test/functional/services/toasts.ts @@ -45,10 +45,21 @@ export function ToastsProvider({ getService }: FtrProviderContext) { public async dismissAllToasts() { const list = await this.getGlobalToastList(); const toasts = await list.findAllByCssSelector(`.euiToast`); + + if (toasts.length === 0) return; + for (const toast of toasts) { await toast.moveMouseTo(); - const dismissButton = await testSubjects.findDescendant('toastCloseButton', toast); - await dismissButton.click(); + + if (await testSubjects.descendantExists('toastCloseButton', toast)) { + try { + const dismissButton = await testSubjects.findDescendant('toastCloseButton', toast); + await dismissButton.click(); + } catch (err) { + // ignore errors + // toasts are finnicky because they can dismiss themselves right before you close them + } + } } } diff --git a/x-pack/test/accessibility/apps/dashboard_edit_panel.ts b/x-pack/test/accessibility/apps/dashboard_edit_panel.ts index 90b3c4ef4d490..c318c2d1c26a0 100644 --- a/x-pack/test/accessibility/apps/dashboard_edit_panel.ts +++ b/x-pack/test/accessibility/apps/dashboard_edit_panel.ts @@ -18,8 +18,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['security', 'common']); const toasts = getService('toasts'); - // Failing: See https://github.com/elastic/kibana/issues/91592 - describe.skip('Dashboard Edit Panel', () => { + const PANEL_TITLE = 'Visualization PieChart'; + + describe('Dashboard Edit Panel', () => { before(async () => { await esArchiver.load('dashboard/drilldowns'); await esArchiver.loadIfNeeded('logstash_functional'); @@ -33,100 +34,68 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.unload('dashboard/drilldowns'); }); - // embeddable edit panel - it(' A11y test on dashboard edit panel menu options', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); + it('can open menu', async () => { + await dashboardPanelActions.openContextMenu(); await a11y.testAppSnapshot(); }); - // https://github.com/elastic/kibana/issues/77931 - it.skip('A11y test for edit visualization and save', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelAction-editPanel'); - await testSubjects.click('visualizesaveAndReturnButton'); + it('can clone panel', async () => { + await dashboardPanelActions.clonePanelByTitle(PANEL_TITLE); await a11y.testAppSnapshot(); + await toasts.dismissAllToasts(); + await dashboardPanelActions.removePanelByTitle(`${PANEL_TITLE} (copy)`); }); - // clone panel - it(' A11y test on dashboard embeddable clone panel', async () => { - await testSubjects.click('embeddablePanelAction-clonePanel'); + it('can customize panel', async () => { + await dashboardPanelActions.customizePanel(); await a11y.testAppSnapshot(); - await toasts.dismissAllToasts(); - await dashboardPanelActions.removePanelByTitle('Visualization PieChart (copy)'); }); - // edit panel title - it(' A11y test on dashboard embeddable edit dashboard title', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelAction-ACTION_CUSTOMIZE_PANEL'); - await a11y.testAppSnapshot(); - await testSubjects.click('customizePanelHideTitle'); + it('can hide panel title', async () => { + await dashboardPanelActions.clickHidePanelTitleToggle(); await a11y.testAppSnapshot(); await testSubjects.click('saveNewTitleButton'); }); - // create drilldown - it('A11y test on dashboard embeddable open flyout and drilldown', async () => { + it('can drilldown', async () => { await testSubjects.click('embeddablePanelToggleMenuIcon'); await testSubjects.click('embeddablePanelAction-OPEN_FLYOUT_ADD_DRILLDOWN'); await a11y.testAppSnapshot(); await testSubjects.click('flyoutCloseButton'); }); - // clicking on more button - it('A11y test on dashboard embeddable more button', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelMore-mainMenu'); + it('can view more actions', async () => { + await dashboardPanelActions.openContextMenuMorePanel(); await a11y.testAppSnapshot(); }); - // https://github.com/elastic/kibana/issues/77422 - it.skip('A11y test on dashboard embeddable custom time range', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); + it('can create a custom time range', async () => { + await dashboardPanelActions.openContextMenuMorePanel(); await testSubjects.click('embeddablePanelAction-CUSTOM_TIME_RANGE'); await a11y.testAppSnapshot(); + await testSubjects.click('addPerPanelTimeRangeButton'); }); - // flow will change whenever the custom time range a11y issue gets fixed. - // Will need to click on gear icon and then click on more. - - // inspector panel - it('A11y test on dashboard embeddable open inspector', async () => { - await testSubjects.click('embeddablePanelAction-openInspector'); + it('can open inspector', async () => { + await dashboardPanelActions.openInspector(); await a11y.testAppSnapshot(); await testSubjects.click('euiFlyoutCloseButton'); }); - // fullscreen - it('A11y test on dashboard embeddable fullscreen', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelMore-mainMenu'); - await testSubjects.click('embeddablePanelAction-togglePanel'); - await a11y.testAppSnapshot(); - }); - - // minimize fullscreen panel - it('A11y test on dashboard embeddable fullscreen minimize ', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelMore-mainMenu'); - await testSubjects.click('embeddablePanelAction-togglePanel'); + it('can go fullscreen', async () => { + await dashboardPanelActions.clickExpandPanelToggle(); await a11y.testAppSnapshot(); + await dashboardPanelActions.clickExpandPanelToggle(); }); - // replace panel - it('A11y test on dashboard embeddable replace panel', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelMore-mainMenu'); - await testSubjects.click('embeddablePanelAction-replacePanel'); + it('can replace panel', async () => { + await dashboardPanelActions.replacePanelByTitle(); await a11y.testAppSnapshot(); await testSubjects.click('euiFlyoutCloseButton'); }); - // delete from dashboard - it('A11y test on dashboard embeddable delete panel', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelMore-mainMenu'); - await testSubjects.click('embeddablePanelAction-deletePanel'); + it('can delete panel', async () => { + await dashboardPanelActions.removePanel(); await a11y.testAppSnapshot(); }); }); diff --git a/x-pack/test/accessibility/apps/uptime.ts b/x-pack/test/accessibility/apps/uptime.ts index 874ede0b13ee9..9c48e7d82788f 100644 --- a/x-pack/test/accessibility/apps/uptime.ts +++ b/x-pack/test/accessibility/apps/uptime.ts @@ -17,10 +17,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const uptimeService = getService('uptime'); const esArchiver = getService('esArchiver'); const es = getService('es'); + const toasts = getService('toasts'); - // FLAKY: https://github.com/elastic/kibana/issues/90555 - // Failing: See https://github.com/elastic/kibana/issues/90555 - describe.skip('uptime', () => { + describe('uptime', () => { before(async () => { await esArchiver.load('uptime/blank'); await makeChecks(es, A11Y_TEST_MONITOR_ID, 150, 1, 1000, { @@ -61,7 +60,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('overview alert popover controls', async () => { await uptimeService.overview.openAlertsPopover(); + await toasts.dismissAllToasts(); await a11y.testAppSnapshot(); + }); + + it('overview alert popover controls nested content', async () => { await uptimeService.overview.navigateToNestedPopover(); await a11y.testAppSnapshot(); }); From d5e025e089ec417f41d72171b8d449ec1dad0119 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Sat, 20 Feb 2021 19:24:23 -0600 Subject: [PATCH 30/32] A few more environment uiFilters fixes (#92044) Some places in the service map and annotations were still attempting to use environment from uiFilters. Fix these. --- .../app/ServiceMap/Popover/ServiceStatsFetcher.tsx | 7 +++---- .../apm/public/context/annotations/annotations_context.tsx | 5 ++--- .../lib/service_map/get_service_map_service_node_info.ts | 4 ++-- x-pack/plugins/apm/server/routes/service_map.ts | 6 ++++-- .../apm_api_integration/tests/service_maps/service_maps.ts | 1 - 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx index 5433ef66d4f99..a71f299ab296c 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx @@ -32,8 +32,7 @@ export function ServiceStatsFetcher({ serviceAnomalyStats, }: ServiceStatsFetcherProps) { const { - urlParams: { start, end }, - uiFilters, + urlParams: { environment, start, end }, } = useUrlParams(); const { @@ -46,12 +45,12 @@ export function ServiceStatsFetcher({ endpoint: 'GET /api/apm/service-map/service/{serviceName}', params: { path: { serviceName }, - query: { start, end, uiFilters: JSON.stringify(uiFilters) }, + query: { environment, start, end }, }, }); } }, - [serviceName, start, end, uiFilters], + [environment, serviceName, start, end], { preservePreviousData: false, } diff --git a/x-pack/plugins/apm/public/context/annotations/annotations_context.tsx b/x-pack/plugins/apm/public/context/annotations/annotations_context.tsx index 435be2cc7133c..54a1d3a59eb20 100644 --- a/x-pack/plugins/apm/public/context/annotations/annotations_context.tsx +++ b/x-pack/plugins/apm/public/context/annotations/annotations_context.tsx @@ -23,9 +23,8 @@ export function AnnotationsContextProvider({ children: React.ReactNode; }) { const { serviceName } = useParams<{ serviceName?: string }>(); - const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; - const { environment } = uiFilters; + const { urlParams } = useUrlParams(); + const { environment, start, end } = urlParams; const { data = INITIAL_STATE } = useFetcher( (callApmApi) => { diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index e384b15685dad..367fbc6810a7f 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -56,7 +56,7 @@ export function getServiceMapServiceNodeInfo({ searchAggregatedTransactions, }: Options & { serviceName: string }) { return withApmSpan('get_service_map_node_stats', async () => { - const { start, end, uiFilters } = setup; + const { start, end } = setup; const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, @@ -66,7 +66,7 @@ export function getServiceMapServiceNodeInfo({ const minutes = Math.abs((end - start) / (1000 * 60)); const taskParams = { - environment: uiFilters.environment, + environment, filter, searchAggregatedTransactions, minutes, diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index 65c7b245958f3..6a05431c5677a 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -12,7 +12,7 @@ import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceMap } from '../lib/service_map/get_service_map'; import { getServiceMapServiceNodeInfo } from '../lib/service_map/get_service_map_service_node_info'; import { createRoute } from './create_route'; -import { environmentRt, rangeRt, uiFiltersRt } from './default_api_types'; +import { environmentRt, rangeRt } from './default_api_types'; import { notifyFeatureUsage } from '../feature'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { isActivePlatinumLicense } from '../../common/license_check'; @@ -67,7 +67,7 @@ export const serviceMapServiceNodeRoute = createRoute({ path: t.type({ serviceName: t.string, }), - query: t.intersection([rangeRt, uiFiltersRt]), + query: t.intersection([environmentRt, rangeRt]), }), options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { @@ -81,6 +81,7 @@ export const serviceMapServiceNodeRoute = createRoute({ const { path: { serviceName }, + query: { environment }, } = context.params; const searchAggregatedTransactions = await getSearchAggregatedTransactions( @@ -88,6 +89,7 @@ export const serviceMapServiceNodeRoute = createRoute({ ); return getServiceMapServiceNodeInfo({ + environment, setup, serviceName, searchAggregatedTransactions, diff --git a/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts b/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts index 5adbafc07e187..f452514cb5172 100644 --- a/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts +++ b/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts @@ -49,7 +49,6 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) const q = querystring.stringify({ start: metadata.start, end: metadata.end, - uiFilters: encodeURIComponent('{}'), }); const response = await supertest.get(`/api/apm/service-map/service/opbeans-node?${q}`); From 65be9e0306a0d4344f473fcd1da46dbc57e2f9c9 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Sun, 21 Feb 2021 10:02:04 +0100 Subject: [PATCH 31/32] [Lens] Drag and drop performance improvements (#91641) --- .../lens/public/drag_drop/drag_drop.scss | 3 + .../lens/public/drag_drop/drag_drop.test.tsx | 82 ++- .../lens/public/drag_drop/drag_drop.tsx | 93 ++-- .../{ => providers}/announcements.tsx | 9 +- .../lens/public/drag_drop/providers/index.tsx | 11 + .../{ => providers}/providers.test.tsx | 2 +- .../drag_drop/{ => providers}/providers.tsx | 219 +------- .../drag_drop/providers/reorder_provider.tsx | 85 +++ .../lens/public/drag_drop/providers/types.tsx | 75 +++ .../draggable_dimension_button.tsx | 21 +- .../config_panel/empty_dimension_button.tsx | 16 +- .../config_panel/layer_panel.test.tsx | 21 +- .../editor_frame/config_panel/layer_panel.tsx | 516 +++++++++--------- .../editor_frame/config_panel/types.ts | 2 - .../workspace_panel/chart_switch.tsx | 6 +- .../workspace_panel/workspace_panel.test.tsx | 1 + .../workspace_panel/workspace_panel.tsx | 53 +- .../dimension_panel/droppable.test.ts | 172 ++---- .../dimension_panel/droppable.ts | 8 +- .../indexpattern_datasource/indexpattern.tsx | 30 +- .../public/indexpattern_datasource/mocks.ts | 1 + x-pack/plugins/lens/public/types.ts | 10 +- 22 files changed, 694 insertions(+), 742 deletions(-) rename x-pack/plugins/lens/public/drag_drop/{ => providers}/announcements.tsx (98%) create mode 100644 x-pack/plugins/lens/public/drag_drop/providers/index.tsx rename x-pack/plugins/lens/public/drag_drop/{ => providers}/providers.test.tsx (94%) rename x-pack/plugins/lens/public/drag_drop/{ => providers}/providers.tsx (53%) create mode 100644 x-pack/plugins/lens/public/drag_drop/providers/reorder_provider.tsx create mode 100644 x-pack/plugins/lens/public/drag_drop/providers/types.tsx diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss index f43063d45a47c..f029c776e8452 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss @@ -45,6 +45,9 @@ .lnsDragDrop-isDropTarget { @include lnsDroppable; @include lnsDroppableActive; + > * { + pointer-events: none; + } } .lnsDragDrop-isActiveGroup { diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx index f2a2fda730388..2fc5efaa28b83 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx @@ -14,7 +14,7 @@ import { ReorderProvider, DragDropIdentifier, DraggingIdentifier, - DropTargets, + DropIdentifier, } from './providers'; import { act } from 'react-dom/test-utils'; import { DropType } from '../types'; @@ -32,6 +32,7 @@ describe('DragDrop', () => { setDragging: jest.fn(), setActiveDropTarget: jest.fn(), activeDropTarget: undefined, + dropTargetsByOrder: undefined, keyboardMode: false, setKeyboardMode: () => {}, setA11yMessage: jest.fn(), @@ -255,11 +256,10 @@ describe('DragDrop', () => { dragging = { id: '1', humanData: { label: 'Label1' } }; }} setActiveDropTarget={setActiveDropTarget} - activeDropTarget={ - ({ activeDropTarget: value } as unknown) as DragContextState['activeDropTarget'] - } + activeDropTarget={value as DragContextState['activeDropTarget']} keyboardMode={false} setKeyboardMode={(keyboardMode) => true} + dropTargetsByOrder={undefined} registerDropTarget={jest.fn()} > { dragging: { ...items[0].value, ghost: { children:

, style: {} } }, setActiveDropTarget, setA11yMessage, - activeDropTarget: { - activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, - dropTargetsByOrder: { - '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, - '2,0,2,0': { ...items[2].value, onDrop, dropType: 'replace_compatible' }, - }, + activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, + dropTargetsByOrder: { + '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, + '2,0,2,0': { ...items[2].value, onDrop, dropType: 'replace_compatible' }, }, keyboardMode: true, }} @@ -463,11 +461,9 @@ describe('DragDrop', () => { dragging: { ...items[0].value, ghost: { children:
Hello
, style: {} } }, setActiveDropTarget, setA11yMessage, - activeDropTarget: { - activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, - dropTargetsByOrder: { - '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, - }, + activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' }, + dropTargetsByOrder: { + '2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' }, }, keyboardMode: true, }} @@ -525,11 +521,12 @@ describe('DragDrop', () => { keyboardMode = mode; }), setActiveDropTarget: (target?: DragDropIdentifier) => { - activeDropTarget = { activeDropTarget: target } as DropTargets; + activeDropTarget = target as DropIdentifier; }, activeDropTarget, setA11yMessage, registerDropTarget, + dropTargetsByOrder: undefined, }; const dragDropSharedProps = { @@ -665,13 +662,11 @@ describe('DragDrop', () => { const component = mountComponent({ dragging: { ...items[0] }, keyboardMode: true, - activeDropTarget: { - activeDropTarget: undefined, - dropTargetsByOrder: { - '2,0,0': undefined, - '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, - '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, - }, + activeDropTarget: undefined, + dropTargetsByOrder: { + '2,0,0': undefined, + '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, + '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, }, setActiveDropTarget, setA11yMessage, @@ -693,15 +688,12 @@ describe('DragDrop', () => { test(`Keyboard navigation: user can drop element to an activeDropTarget`, () => { const component = mountComponent({ dragging: { ...items[0] }, - activeDropTarget: { - activeDropTarget: { ...items[2], dropType: 'reorder', onDrop }, - dropTargetsByOrder: { - '2,0,0': { ...items[0], onDrop, dropType: 'reorder' }, - '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, - '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, - }, + activeDropTarget: { ...items[2], dropType: 'reorder', onDrop }, + dropTargetsByOrder: { + '2,0,0': { ...items[0], onDrop, dropType: 'reorder' }, + '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, + '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, }, - keyboardMode: true, }); const keyboardHandler = component @@ -747,13 +739,11 @@ describe('DragDrop', () => { const component = mountComponent({ dragging: { ...items[0] }, keyboardMode: true, - activeDropTarget: { - activeDropTarget: undefined, - dropTargetsByOrder: { - '2,0,0': undefined, - '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, - '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, - }, + activeDropTarget: undefined, + dropTargetsByOrder: { + '2,0,0': undefined, + '2,0,1': { ...items[1], onDrop, dropType: 'reorder' }, + '2,0,2': { ...items[2], onDrop, dropType: 'reorder' }, }, setA11yMessage, }); @@ -799,15 +789,13 @@ describe('DragDrop', () => { {...defaultContext} keyboardMode={true} activeDropTarget={{ - activeDropTarget: { - ...items[1], - onDrop, - dropType: 'reorder', - }, - dropTargetsByOrder: { - '2,0,1,0': undefined, - '2,0,1,1': { ...items[1], onDrop, dropType: 'reorder' }, - }, + ...items[1], + onDrop, + dropType: 'reorder', + }} + dropTargetsByOrder={{ + '2,0,1,0': undefined, + '2,0,1,1': { ...items[1], onDrop, dropType: 'reorder' }, }} dragging={{ ...items[0] }} setActiveDropTarget={setActiveDropTarget} diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx index 6c6a65ab421b3..618a7accb9b2b 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx @@ -19,8 +19,8 @@ import { ReorderContext, ReorderState, DropHandler, + announce, } from './providers'; -import { announce } from './announcements'; import { trackUiEvent } from '../lens_ui_telemetry'; import { DropType } from '../types'; @@ -99,13 +99,15 @@ interface BaseProps { * The props for a draggable instance of that component. */ interface DragInnerProps extends BaseProps { - isDragging: boolean; - keyboardMode: boolean; setKeyboardMode: DragContextState['setKeyboardMode']; setDragging: DragContextState['setDragging']; setActiveDropTarget: DragContextState['setActiveDropTarget']; setA11yMessage: DragContextState['setA11yMessage']; - activeDropTarget: DragContextState['activeDropTarget']; + activeDraggingProps?: { + keyboardMode: DragContextState['keyboardMode']; + activeDropTarget: DragContextState['activeDropTarget']; + dropTargetsByOrder: DragContextState['dropTargetsByOrder']; + }; onDragStart?: ( target?: | DroppableEvent['currentTarget'] @@ -121,6 +123,7 @@ interface DragInnerProps extends BaseProps { */ interface DropInnerProps extends BaseProps { dragging: DragContextState['dragging']; + keyboardMode: DragContextState['keyboardMode']; setKeyboardMode: DragContextState['setKeyboardMode']; setDragging: DragContextState['setDragging']; setActiveDropTarget: DragContextState['setActiveDropTarget']; @@ -136,8 +139,9 @@ export const DragDrop = (props: BaseProps) => { const { dragging, setDragging, - registerDropTarget, keyboardMode, + registerDropTarget, + dropTargetsByOrder, setKeyboardMode, activeDropTarget, setActiveDropTarget, @@ -147,34 +151,31 @@ export const DragDrop = (props: BaseProps) => { const { value, draggable, dropType, reorderableGroup } = props; const isDragging = !!(draggable && value.id === dragging?.id); + const activeDraggingProps = isDragging + ? { + keyboardMode, + activeDropTarget, + dropTargetsByOrder, + } + : undefined; + if (draggable && !dropType) { const dragProps = { ...props, - isDragging, - keyboardMode: isDragging ? keyboardMode : false, // optimization to not rerender all dragging components - activeDropTarget: isDragging ? activeDropTarget : undefined, // optimization to not rerender all dragging components + activeDraggingProps, setKeyboardMode, setDragging, setActiveDropTarget, setA11yMessage, }; if (reorderableGroup && reorderableGroup.length > 1) { - return ( - - ); + return ; } else { - return ; + return ; } } - const isActiveDropTarget = Boolean( - activeDropTarget?.activeDropTarget && activeDropTarget.activeDropTarget.id === value.id - ); + const isActiveDropTarget = Boolean(activeDropTarget?.id === value.id); const dropProps = { ...props, keyboardMode, @@ -210,9 +211,7 @@ const DragInner = memo(function DragInner({ setKeyboardMode, setActiveDropTarget, order, - keyboardMode, - isDragging, - activeDropTarget, + activeDraggingProps, dragType, onDragStart, onDragEnd, @@ -220,6 +219,10 @@ const DragInner = memo(function DragInner({ ariaDescribedBy, setA11yMessage, }: DragInnerProps) { + const keyboardMode = activeDraggingProps?.keyboardMode; + const activeDropTarget = activeDraggingProps?.activeDropTarget; + const dropTargetsByOrder = activeDraggingProps?.dropTargetsByOrder; + const dragStart = ( e: DroppableEvent | React.KeyboardEvent, keyboardModeOn?: boolean @@ -273,9 +276,9 @@ const DragInner = memo(function DragInner({ } }; const dropToActiveDropTarget = () => { - if (isDragging && activeDropTarget?.activeDropTarget) { + if (activeDropTarget) { trackUiEvent('drop_total'); - const { dropType, humanData, onDrop: onTargetDrop } = activeDropTarget.activeDropTarget; + const { dropType, humanData, onDrop: onTargetDrop } = activeDropTarget; setTimeout(() => setA11yMessage(announce.dropped(value.humanData, humanData, dropType))); onTargetDrop(value, dropType); } @@ -287,6 +290,7 @@ const DragInner = memo(function DragInner({ } const nextTarget = nextValidDropTarget( + dropTargetsByOrder, activeDropTarget, [order.join(',')], (el) => el?.dropType !== 'reorder', @@ -301,11 +305,10 @@ const DragInner = memo(function DragInner({ ); }; const shouldShowGhostImageInstead = - isDragging && dragType === 'move' && keyboardMode && - activeDropTarget?.activeDropTarget && - activeDropTarget?.activeDropTarget.dropType !== 'reorder'; + activeDropTarget && + activeDropTarget.dropType !== 'reorder'; return (
{ - if (isDragging) { + if (activeDraggingProps) { dragEnd(); } }} @@ -331,13 +334,13 @@ const DragInner = memo(function DragInner({ dropToActiveDropTarget(); } - if (isDragging) { + if (activeDraggingProps) { dragEnd(); } else { dragStart(e, true); } } else if (key === keys.ESCAPE) { - if (isDragging) { + if (activeDraggingProps) { e.stopPropagation(); e.preventDefault(); dragEnd(); @@ -357,7 +360,8 @@ const DragInner = memo(function DragInner({ 'data-test-subj': dataTestSubj || 'lnsDragDrop', className: classNames(children.props.className, 'lnsDragDrop', 'lnsDragDrop-isDraggable', { 'lnsDragDrop-isHidden': - (isDragging && dragType === 'move' && !keyboardMode) || shouldShowGhostImageInstead, + (activeDraggingProps && dragType === 'move' && !keyboardMode) || + shouldShowGhostImageInstead, }), draggable: true, onDragEnd: dragEnd, @@ -384,19 +388,20 @@ const DropInner = memo(function DropInner(props: DropInnerProps) { isActiveDropTarget, registerDropTarget, setActiveDropTarget, + keyboardMode, setKeyboardMode, setDragging, setA11yMessage, } = props; useShallowCompareEffect(() => { - if (dropType && value && onDrop) { + if (dropType && onDrop && keyboardMode) { registerDropTarget(order, { ...value, onDrop, dropType }); return () => { registerDropTarget(order, undefined); }; } - }, [order, value, registerDropTarget, dropType]); + }, [order, value, registerDropTarget, dropType, keyboardMode]); const classesOnEnter = getAdditionalClassesOnEnter?.(dropType); const classesOnDroppable = getAdditionalClassesOnDroppable?.(dropType); @@ -481,17 +486,19 @@ const ReorderableDrag = memo(function ReorderableDrag( const { value, setActiveDropTarget, - keyboardMode, - isDragging, - activeDropTarget, + activeDraggingProps, reorderableGroup, setA11yMessage, } = props; + const keyboardMode = activeDraggingProps?.keyboardMode; + const activeDropTarget = activeDraggingProps?.activeDropTarget; + const dropTargetsByOrder = activeDraggingProps?.dropTargetsByOrder; + const isDragging = !!activeDraggingProps; + const isFocusInGroup = keyboardMode ? isDragging && - (!activeDropTarget?.activeDropTarget || - reorderableGroup.some((i) => i.id === activeDropTarget?.activeDropTarget?.id)) + (!activeDropTarget || reorderableGroup.some((i) => i.id === activeDropTarget?.id)) : isDragging; useEffect(() => { @@ -530,10 +537,8 @@ const ReorderableDrag = memo(function ReorderableDrag( e.stopPropagation(); e.preventDefault(); let activeDropTargetIndex = reorderableGroup.findIndex((i) => i.id === value.id); - if (activeDropTarget?.activeDropTarget) { - const index = reorderableGroup.findIndex( - (i) => i.id === activeDropTarget.activeDropTarget?.id - ); + if (activeDropTarget) { + const index = reorderableGroup.findIndex((i) => i.id === activeDropTarget?.id); if (index !== -1) activeDropTargetIndex = index; } if (e.key === keys.ARROW_LEFT || e.key === keys.ARROW_RIGHT) { @@ -542,6 +547,7 @@ const ReorderableDrag = memo(function ReorderableDrag( } else if (keys.ARROW_DOWN === e.key) { if (activeDropTargetIndex < reorderableGroup.length - 1) { const nextTarget = nextValidDropTarget( + dropTargetsByOrder, activeDropTarget, [props.order.join(',')], (el) => el?.dropType === 'reorder' @@ -551,6 +557,7 @@ const ReorderableDrag = memo(function ReorderableDrag( } else if (keys.ARROW_UP === e.key) { if (activeDropTargetIndex > 0) { const nextTarget = nextValidDropTarget( + dropTargetsByOrder, activeDropTarget, [props.order.join(',')], (el) => el?.dropType === 'reorder', diff --git a/x-pack/plugins/lens/public/drag_drop/announcements.tsx b/x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx similarity index 98% rename from x-pack/plugins/lens/public/drag_drop/announcements.tsx rename to x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx index 3c65008f8f38b..3bd1d5693005c 100644 --- a/x-pack/plugins/lens/public/drag_drop/announcements.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers/announcements.tsx @@ -6,13 +6,8 @@ */ import { i18n } from '@kbn/i18n'; -import { DropType } from '../types'; -export interface HumanData { - label: string; - groupLabel?: string; - position?: number; - nextLabel?: string; -} +import { DropType } from '../../types'; +import { HumanData } from '.'; type AnnouncementFunction = (draggedElement: HumanData, dropElement: HumanData) => string; diff --git a/x-pack/plugins/lens/public/drag_drop/providers/index.tsx b/x-pack/plugins/lens/public/drag_drop/providers/index.tsx new file mode 100644 index 0000000000000..4262b65c85887 --- /dev/null +++ b/x-pack/plugins/lens/public/drag_drop/providers/index.tsx @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './providers'; +export * from './reorder_provider'; +export * from './types'; +export * from './announcements'; diff --git a/x-pack/plugins/lens/public/drag_drop/providers.test.tsx b/x-pack/plugins/lens/public/drag_drop/providers/providers.test.tsx similarity index 94% rename from x-pack/plugins/lens/public/drag_drop/providers.test.tsx rename to x-pack/plugins/lens/public/drag_drop/providers/providers.test.tsx index a46b7f6f95314..a8312cc927451 100644 --- a/x-pack/plugins/lens/public/drag_drop/providers.test.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers/providers.test.tsx @@ -7,7 +7,7 @@ import React, { useContext } from 'react'; import { mount } from 'enzyme'; -import { RootDragDropProvider, DragContext } from './providers'; +import { RootDragDropProvider, DragContext } from '.'; jest.useFakeTimers(); diff --git a/x-pack/plugins/lens/public/drag_drop/providers.tsx b/x-pack/plugins/lens/public/drag_drop/providers/providers.tsx similarity index 53% rename from x-pack/plugins/lens/public/drag_drop/providers.tsx rename to x-pack/plugins/lens/public/drag_drop/providers/providers.tsx index deb9bf6cb17ae..6a78bc1b46ddf 100644 --- a/x-pack/plugins/lens/public/drag_drop/providers.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers/providers.tsx @@ -6,70 +6,15 @@ */ import React, { useState, useMemo } from 'react'; -import classNames from 'classnames'; import { EuiScreenReaderOnly, EuiPortal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { HumanData } from './announcements'; -import { DropType } from '../types'; - -/** - * A function that handles a drop event. - */ -export type DropHandler = (dropped: DragDropIdentifier, dropType?: DropType) => void; - -export type DragDropIdentifier = Record & { - id: string; - /** - * The data for accessibility, consists of required label and not required groupLabel and position in group - */ - humanData: HumanData; -}; - -export type DraggingIdentifier = DragDropIdentifier & { - ghost?: { - children: React.ReactElement; - style: React.CSSProperties; - }; -}; - -export type DropIdentifier = DragDropIdentifier & { - dropType: DropType; - onDrop: DropHandler; -}; - -export interface DropTargets { - activeDropTarget?: DropIdentifier; - dropTargetsByOrder: Record; -} -/** - * The shape of the drag / drop context. - */ -export interface DragContextState { - /** - * The item being dragged or undefined. - */ - dragging?: DraggingIdentifier; - - /** - * keyboard mode - */ - keyboardMode: boolean; - /** - * keyboard mode - */ - setKeyboardMode: (mode: boolean) => void; - /** - * Set the item being dragged. - */ - setDragging: (dragging?: DraggingIdentifier) => void; - - activeDropTarget?: DropTargets; - - setActiveDropTarget: (newTarget?: DropIdentifier) => void; - - setA11yMessage: (message: string) => void; - registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void; -} +import { + DropIdentifier, + DraggingIdentifier, + DragDropIdentifier, + RegisteredDropTargets, + DragContextState, +} from './types'; /** * The drag / drop context singleton, used like so: @@ -84,51 +29,18 @@ export const DragContext = React.createContext({ activeDropTarget: undefined, setActiveDropTarget: () => {}, setA11yMessage: () => {}, + dropTargetsByOrder: undefined, registerDropTarget: () => {}, }); /** * The argument to DragDropProvider. */ -export interface ProviderProps { - /** - * keyboard mode - */ - keyboardMode: boolean; - /** - * keyboard mode - */ - setKeyboardMode: (mode: boolean) => void; - /** - * Set the item being dragged. - */ - /** - * The item being dragged. If unspecified, the provider will - * behave as if it is the root provider. - */ - dragging?: DraggingIdentifier; - - /** - * Sets the item being dragged. If unspecified, the provider - * will behave as if it is the root provider. - */ - setDragging: (dragging?: DraggingIdentifier) => void; - - activeDropTarget?: { - activeDropTarget?: DropIdentifier; - dropTargetsByOrder: Record; - }; - - setActiveDropTarget: (newTarget?: DropIdentifier) => void; - - registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void; - +export interface ProviderProps extends DragContextState { /** * The React children. */ children: React.ReactNode; - - setA11yMessage: (message: string) => void; } /** @@ -144,13 +56,11 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } }); const [keyboardModeState, setKeyboardModeState] = useState(false); const [a11yMessageState, setA11yMessageState] = useState(''); - const [activeDropTargetState, setActiveDropTargetState] = useState<{ - activeDropTarget?: DropIdentifier; - dropTargetsByOrder: Record; - }>({ - activeDropTarget: undefined, - dropTargetsByOrder: {}, - }); + const [activeDropTargetState, setActiveDropTargetState] = useState( + undefined + ); + + const [dropTargetsByOrderState, setDropTargetsByOrderState] = useState({}); const setDragging = useMemo( () => (dragging?: DraggingIdentifier) => setDraggingState({ dragging }), @@ -162,24 +72,20 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } ]); const setActiveDropTarget = useMemo( - () => (activeDropTarget?: DropIdentifier) => - setActiveDropTargetState((s) => ({ ...s, activeDropTarget })), + () => (activeDropTarget?: DropIdentifier) => setActiveDropTargetState(activeDropTarget), [setActiveDropTargetState] ); const registerDropTarget = useMemo( () => (order: number[], dropTarget?: DropIdentifier) => { - return setActiveDropTargetState((s) => { + return setDropTargetsByOrderState((s) => { return { ...s, - dropTargetsByOrder: { - ...s.dropTargetsByOrder, - [order.join(',')]: dropTarget, - }, + [order.join(',')]: dropTarget, }; }); }, - [setActiveDropTargetState] + [setDropTargetsByOrderState] ); return ( @@ -193,6 +99,7 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } activeDropTarget={activeDropTargetState} setActiveDropTarget={setActiveDropTarget} registerDropTarget={registerDropTarget} + dropTargetsByOrder={dropTargetsByOrderState} > {children} @@ -220,16 +127,17 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } } export function nextValidDropTarget( - activeDropTarget: DropTargets | undefined, + dropTargetsByOrder: RegisteredDropTargets, + activeDropTarget: DropIdentifier | undefined, draggingOrder: [string], filterElements: (el: DragDropIdentifier) => boolean = () => true, reverse = false ) { - if (!activeDropTarget) { + if (!dropTargetsByOrder) { return; } - const filteredTargets = [...Object.entries(activeDropTarget.dropTargetsByOrder)].filter( + const filteredTargets = Object.entries(dropTargetsByOrder).filter( ([, dropTarget]) => dropTarget && filterElements(dropTarget) ); @@ -242,7 +150,7 @@ export function nextValidDropTarget( }); let currentActiveDropIndex = nextDropTargets.findIndex( - ([_, dropTarget]) => dropTarget?.id === activeDropTarget?.activeDropTarget?.id + ([_, dropTarget]) => dropTarget?.id === activeDropTarget?.id ); if (currentActiveDropIndex === -1) { @@ -274,6 +182,7 @@ export function ChildDragDropProvider({ setActiveDropTarget, setA11yMessage, registerDropTarget, + dropTargetsByOrder, children, }: ProviderProps) { const value = useMemo( @@ -285,6 +194,7 @@ export function ChildDragDropProvider({ activeDropTarget, setActiveDropTarget, setA11yMessage, + dropTargetsByOrder, registerDropTarget, }), [ @@ -295,84 +205,9 @@ export function ChildDragDropProvider({ setKeyboardMode, keyboardMode, setA11yMessage, + dropTargetsByOrder, registerDropTarget, ] ); return {children}; } - -export interface ReorderState { - /** - * Ids of the elements that are translated up or down - */ - reorderedItems: Array<{ id: string; height?: number }>; - - /** - * Direction of the move of dragged element in the reordered list - */ - direction: '-' | '+'; - /** - * height of the dragged element - */ - draggingHeight: number; - /** - * indicates that user is in keyboard mode - */ - isReorderOn: boolean; - /** - * reorder group needed for screen reader aria-described-by attribute - */ - groupId: string; -} - -type SetReorderStateDispatch = (prevState: ReorderState) => ReorderState; - -export interface ReorderContextState { - reorderState: ReorderState; - setReorderState: (dispatch: SetReorderStateDispatch) => void; -} - -export const ReorderContext = React.createContext({ - reorderState: { - reorderedItems: [], - direction: '-', - draggingHeight: 40, - isReorderOn: false, - groupId: '', - }, - setReorderState: () => () => {}, -}); - -export function ReorderProvider({ - id, - children, - className, -}: { - id: string; - children: React.ReactNode; - className?: string; -}) { - const [state, setState] = useState({ - reorderedItems: [], - direction: '-', - draggingHeight: 40, - isReorderOn: false, - groupId: id, - }); - - const setReorderState = useMemo(() => (dispatch: SetReorderStateDispatch) => setState(dispatch), [ - setState, - ]); - return ( -
1, - })} - > - - {children} - -
- ); -} diff --git a/x-pack/plugins/lens/public/drag_drop/providers/reorder_provider.tsx b/x-pack/plugins/lens/public/drag_drop/providers/reorder_provider.tsx new file mode 100644 index 0000000000000..77620ea131513 --- /dev/null +++ b/x-pack/plugins/lens/public/drag_drop/providers/reorder_provider.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useMemo } from 'react'; +import classNames from 'classnames'; + +export interface ReorderState { + /** + * Ids of the elements that are translated up or down + */ + reorderedItems: Array<{ id: string; height?: number }>; + + /** + * Direction of the move of dragged element in the reordered list + */ + direction: '-' | '+'; + /** + * height of the dragged element + */ + draggingHeight: number; + /** + * indicates that user is in keyboard mode + */ + isReorderOn: boolean; + /** + * reorder group needed for screen reader aria-described-by attribute + */ + groupId: string; +} + +type SetReorderStateDispatch = (prevState: ReorderState) => ReorderState; + +export interface ReorderContextState { + reorderState: ReorderState; + setReorderState: (dispatch: SetReorderStateDispatch) => void; +} + +export const ReorderContext = React.createContext({ + reorderState: { + reorderedItems: [], + direction: '-', + draggingHeight: 40, + isReorderOn: false, + groupId: '', + }, + setReorderState: () => () => {}, +}); + +export function ReorderProvider({ + id, + children, + className, +}: { + id: string; + children: React.ReactNode; + className?: string; +}) { + const [state, setState] = useState({ + reorderedItems: [], + direction: '-', + draggingHeight: 40, + isReorderOn: false, + groupId: id, + }); + + const setReorderState = useMemo(() => (dispatch: SetReorderStateDispatch) => setState(dispatch), [ + setState, + ]); + return ( +
1, + })} + > + + {children} + +
+ ); +} diff --git a/x-pack/plugins/lens/public/drag_drop/providers/types.tsx b/x-pack/plugins/lens/public/drag_drop/providers/types.tsx new file mode 100644 index 0000000000000..11f460a400dcd --- /dev/null +++ b/x-pack/plugins/lens/public/drag_drop/providers/types.tsx @@ -0,0 +1,75 @@ +/* + * Copyright 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 { DropType } from '../../types'; + +export interface HumanData { + label: string; + groupLabel?: string; + position?: number; + nextLabel?: string; +} + +export type DragDropIdentifier = Record & { + id: string; + /** + * The data for accessibility, consists of required label and not required groupLabel and position in group + */ + humanData: HumanData; +}; + +export type DraggingIdentifier = DragDropIdentifier & { + ghost?: { + children: React.ReactElement; + style: React.CSSProperties; + }; +}; + +export type DropIdentifier = DragDropIdentifier & { + dropType: DropType; + onDrop: DropHandler; +}; + +/** + * A function that handles a drop event. + */ +export type DropHandler = (dropped: DragDropIdentifier, dropType?: DropType) => void; + +export type RegisteredDropTargets = Record | undefined; + +/** + * The shape of the drag / drop context. + */ + +export interface DragContextState { + /** + * The item being dragged or undefined. + */ + dragging?: DraggingIdentifier; + + /** + * keyboard mode + */ + keyboardMode: boolean; + /** + * keyboard mode + */ + setKeyboardMode: (mode: boolean) => void; + /** + * Set the item being dragged. + */ + setDragging: (dragging?: DraggingIdentifier) => void; + + activeDropTarget?: DropIdentifier; + + dropTargetsByOrder: RegisteredDropTargets; + + setActiveDropTarget: (newTarget?: DropIdentifier) => void; + + setA11yMessage: (message: string) => void; + registerDropTarget: (order: number[], dropTarget?: DropIdentifier) => void; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx index 1cbd41fff2a8f..04ab1318a12e0 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import React, { useMemo, useCallback } from 'react'; -import { DragDrop, DragDropIdentifier, DragContextState } from '../../../drag_drop'; +import React, { useMemo, useCallback, useContext } from 'react'; +import { DragDrop, DragDropIdentifier, DragContext } from '../../../drag_drop'; + import { Datasource, VisualizationDimensionGroupConfig, @@ -41,12 +42,10 @@ export function DraggableDimensionButton({ group, onDrop, children, - dragDropContext, layerDatasourceDropProps, layerDatasource, registerNewButtonRef, }: { - dragDropContext: DragContextState; layerId: string; groupIndex: number; layerIndex: number; @@ -64,8 +63,11 @@ export function DraggableDimensionButton({ columnId: string; registerNewButtonRef: (id: string, instance: HTMLDivElement | null) => void; }) { + const { dragging } = useContext(DragContext); + const dropProps = layerDatasource.getDropProps({ ...layerDatasourceDropProps, + dragging, columnId, filterOperations: group.filterOperations, groupId: group.groupId, @@ -105,6 +107,11 @@ export function DraggableDimensionButton({ columnId, ]); + const handleOnDrop = React.useCallback( + (droppedItem, selectedDropType) => onDrop(droppedItem, value, selectedDropType), + [value, onDrop] + ); + return (
1 ? reorderableGroup : undefined} value={value} - onDrop={(drag: DragDropIdentifier, selectedDropType?: DropType) => - onDrop(drag, value, selectedDropType) - } + onDrop={handleOnDrop} > {children} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx index c9d0a7b002870..664e24b989836 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx @@ -5,12 +5,13 @@ * 2.0. */ -import React, { useMemo, useState, useEffect } from 'react'; +import React, { useMemo, useState, useEffect, useContext } from 'react'; import { EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { generateId } from '../../../id_generator'; -import { DragDrop, DragDropIdentifier } from '../../../drag_drop'; +import { DragDrop, DragDropIdentifier, DragContext } from '../../../drag_drop'; + import { Datasource, VisualizationDimensionGroupConfig, DropType } from '../../../types'; import { LayerDatasourceDropProps } from './types'; @@ -47,6 +48,8 @@ export function EmptyDimensionButton({ layerDatasource: Datasource; layerDatasourceDropProps: LayerDatasourceDropProps; }) { + const { dragging } = useContext(DragContext); + const itemIndex = group.accessors.length; const [newColumnId, setNewColumnId] = useState(generateId()); @@ -56,6 +59,7 @@ export function EmptyDimensionButton({ const dropProps = layerDatasource.getDropProps({ ...layerDatasourceDropProps, + dragging, columnId: newColumnId, filterOperations: group.filterOperations, groupId: group.groupId, @@ -81,14 +85,18 @@ export function EmptyDimensionButton({ [dropType, newColumnId, group.groupId, layerId, group.groupLabel, itemIndex, nextLabel] ); + const handleOnDrop = React.useCallback( + (droppedItem, selectedDropType) => onDrop(droppedItem, value, selectedDropType), + [value, onDrop] + ); + return (
onDrop(droppedItem, value, selectedDropType)} + onDrop={handleOnDrop} dropType={dropType} >
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index 619147987cdd5..52726afcffe8d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -28,6 +28,7 @@ const defaultContext = { setDragging: jest.fn(), setActiveDropTarget: () => {}, activeDropTarget: undefined, + dropTargetsByOrder: undefined, keyboardMode: false, setKeyboardMode: () => {}, setA11yMessage: jest.fn(), @@ -464,9 +465,7 @@ describe('LayerPanel', () => { expect(mockDatasource.getDropProps).toHaveBeenCalledWith( expect.objectContaining({ - dragDropContext: expect.objectContaining({ - dragging: draggingField, - }), + dragging: draggingField, }) ); @@ -474,9 +473,7 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ - dragDropContext: expect.objectContaining({ - dragging: draggingField, - }), + droppedItem: draggingField, }) ); }); @@ -582,9 +579,7 @@ describe('LayerPanel', () => { expect(mockDatasource.getDropProps).toHaveBeenCalledWith( expect.objectContaining({ - dragDropContext: expect.objectContaining({ - dragging: draggingOperation, - }), + dragging: draggingOperation, }) ); @@ -593,9 +588,7 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ columnId: 'b', - dragDropContext: expect.objectContaining({ - dragging: draggingOperation, - }), + droppedItem: draggingOperation, }) ); @@ -604,9 +597,7 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ columnId: 'newid', - dragDropContext: expect.objectContaining({ - dragging: draggingOperation, - }), + droppedItem: draggingOperation, }) ); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 5d84f826ab988..59b64de369745 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -7,17 +7,12 @@ import './layer_panel.scss'; -import React, { useContext, useState, useEffect, useMemo, useCallback } from 'react'; +import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NativeRenderer } from '../../../native_renderer'; import { StateSetter, Visualization, DraggedOperation, DropType } from '../../../types'; -import { - DragContext, - DragDropIdentifier, - ChildDragDropProvider, - ReorderProvider, -} from '../../../drag_drop'; +import { DragDropIdentifier, ReorderProvider } from '../../../drag_drop'; import { LayerSettings } from './layer_settings'; import { trackUiEvent } from '../../../lens_ui_telemetry'; import { LayerPanelProps, ActiveDimensionState } from './types'; @@ -49,7 +44,6 @@ export function LayerPanel( registerNewLayerRef: (layerId: string, instance: HTMLDivElement | null) => void; } ) { - const dragDropContext = useContext(DragContext); const [activeDimension, setActiveDimension] = useState( initialActiveDimensionState ); @@ -78,7 +72,6 @@ export function LayerPanel( const layerVisualizationConfigProps = { layerId, - dragDropContext, state: props.visualizationState, frame: props.framePublicAPI, dateRange: props.framePublicAPI.dateRange, @@ -91,13 +84,12 @@ export function LayerPanel( const layerDatasourceDropProps = useMemo( () => ({ layerId, - dragDropContext, state: layerDatasourceState, setState: (newState: unknown) => { updateDatasource(datasourceId, newState); }, }), - [layerId, dragDropContext, layerDatasourceState, datasourceId, updateDatasource] + [layerId, layerDatasourceState, datasourceId, updateDatasource] ); const layerDatasource = props.datasourceMap[datasourceId]; @@ -116,7 +108,6 @@ export function LayerPanel( const columnLabelMap = layerDatasource.uniqueLabels(layerDatasourceConfigProps.state); const { setDimension, removeDimension } = activeVisualization; - const layerDatasourceOnDrop = layerDatasource.onDrop; const allAccessors = groups.flatMap((group) => group.accessors.map((accessor) => accessor.columnId) @@ -128,6 +119,8 @@ export function LayerPanel( registerNewRef: registerNewButtonRef, } = useFocusUpdate(allAccessors); + const layerDatasourceOnDrop = layerDatasource.onDrop; + const onDrop = useMemo(() => { return ( droppedItem: DragDropIdentifier, @@ -194,275 +187,272 @@ export function LayerPanel( ]); return ( - -
- - - - - +
+ + + + + - {layerDatasource && ( - - { - const newState = - typeof updater === 'function' ? updater(layerDatasourceState) : updater; - // Look for removed columns - const nextPublicAPI = layerDatasource.getPublicAPI({ - state: newState, + {layerDatasource && ( + + { + const newState = + typeof updater === 'function' ? updater(layerDatasourceState) : updater; + // Look for removed columns + const nextPublicAPI = layerDatasource.getPublicAPI({ + state: newState, + layerId, + }); + const nextTable = new Set( + nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) + ); + const removed = datasourcePublicAPI + .getTableSpec() + .map(({ columnId }) => columnId) + .filter((columnId) => !nextTable.has(columnId)); + let nextVisState = props.visualizationState; + removed.forEach((columnId) => { + nextVisState = activeVisualization.removeDimension({ layerId, + columnId, + prevState: nextVisState, }); - const nextTable = new Set( - nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) - ); - const removed = datasourcePublicAPI - .getTableSpec() - .map(({ columnId }) => columnId) - .filter((columnId) => !nextTable.has(columnId)); - let nextVisState = props.visualizationState; - removed.forEach((columnId) => { - nextVisState = activeVisualization.removeDimension({ - layerId, - columnId, - prevState: nextVisState, - }); - }); + }); - props.updateAll(datasourceId, newState, nextVisState); - }, - }} - /> - - )} - - - + props.updateAll(datasourceId, newState, nextVisState); + }, + }} + /> + + )} + - {groups.map((group, groupIndex) => { - const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; - return ( - {group.groupLabel}
} - labelType="legend" - key={group.groupId} - isInvalid={isMissing} - error={ - isMissing ? ( -
- {i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', { - defaultMessage: 'Required dimension', - })} -
- ) : ( - [] - ) - } - > - <> - - {group.accessors.map((accessorConfig, accessorIndex) => { - const { columnId } = accessorConfig; + - return ( - -
- { - setActiveDimension({ - isNew: false, - activeGroup: group, - activeId: id, - }); - }} - onRemoveClick={(id: string) => { - trackUiEvent('indexpattern_dimension_removed'); - props.updateAll( - datasourceId, - layerDatasource.removeColumn({ - layerId, - columnId: id, - prevState: layerDatasourceState, - }), - activeVisualization.removeDimension({ - layerId, - columnId: id, - prevState: props.visualizationState, - }) - ); - removeButtonRef(id); - }} - > - - -
-
- ); + {groups.map((group, groupIndex) => { + const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; + return ( + {group.groupLabel}
} + labelType="legend" + key={group.groupId} + isInvalid={isMissing} + error={ + isMissing ? ( +
+ {i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', { + defaultMessage: 'Required dimension', })} - - {group.supportsMoreColumns ? ( - { - setActiveDimension({ - activeGroup: group, - activeId: id, - isNew: true, - }); - }} - onDrop={onDrop} - /> - ) : null} - - - ); - })} - { - if (layerDatasource.updateStateOnCloseDimension) { - const newState = layerDatasource.updateStateOnCloseDimension({ - state: layerDatasourceState, - layerId, - columnId: activeId!, - }); - if (newState) { - props.updateDatasource(datasourceId, newState); - } +
+ ) : ( + [] + ) } - setActiveDimension(initialActiveDimensionState); - }} - panel={ + > <> - {activeGroup && activeId && ( - { - if (shouldReplaceDimension || shouldRemoveDimension) { - props.updateAll( - datasourceId, - newState, - shouldRemoveDimension - ? activeVisualization.removeDimension({ + + {group.accessors.map((accessorConfig, accessorIndex) => { + const { columnId } = accessorConfig; + + return ( + +
+ { + setActiveDimension({ + isNew: false, + activeGroup: group, + activeId: id, + }); + }} + onRemoveClick={(id: string) => { + trackUiEvent('indexpattern_dimension_removed'); + props.updateAll( + datasourceId, + layerDatasource.removeColumn({ layerId, - columnId: activeId, - prevState: props.visualizationState, - }) - : activeVisualization.setDimension({ + columnId: id, + prevState: layerDatasourceState, + }), + activeVisualization.removeDimension({ layerId, - groupId: activeGroup.groupId, - columnId: activeId, + columnId: id, prevState: props.visualizationState, }) - ); - } else { - props.updateDatasource(datasourceId, newState); - } - setActiveDimension({ - ...activeDimension, - isNew: false, - }); - }, + ); + removeButtonRef(id); + }} + > + + +
+
+ ); + })} +
+ {group.supportsMoreColumns ? ( + { + setActiveDimension({ + activeGroup: group, + activeId: id, + isNew: true, + }); }} + onDrop={onDrop} /> - )} - {activeGroup && - activeId && - !activeDimension.isNew && - activeVisualization.renderDimensionEditor && - activeGroup?.enableDimensionEditor && ( -
- -
- )} + ) : null} + + ); + })} + { + if (layerDatasource.updateStateOnCloseDimension) { + const newState = layerDatasource.updateStateOnCloseDimension({ + state: layerDatasourceState, + layerId, + columnId: activeId!, + }); + if (newState) { + props.updateDatasource(datasourceId, newState); + } } - /> + setActiveDimension(initialActiveDimensionState); + }} + panel={ + <> + {activeGroup && activeId && ( + { + if (shouldReplaceDimension || shouldRemoveDimension) { + props.updateAll( + datasourceId, + newState, + shouldRemoveDimension + ? activeVisualization.removeDimension({ + layerId, + columnId: activeId, + prevState: props.visualizationState, + }) + : activeVisualization.setDimension({ + layerId, + groupId: activeGroup.groupId, + columnId: activeId, + prevState: props.visualizationState, + }) + ); + } else { + props.updateDatasource(datasourceId, newState); + } + setActiveDimension({ + ...activeDimension, + isNew: false, + }); + }, + }} + /> + )} + {activeGroup && + activeId && + !activeDimension.isNew && + activeVisualization.renderDimensionEditor && + activeGroup?.enableDimensionEditor && ( +
+ +
+ )} + + } + /> - + - - - - - - - - + + + + + + + ); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts index 22e28292b8da7..37b2198cfd51f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts @@ -13,7 +13,6 @@ import { DatasourceDimensionEditorProps, VisualizationDimensionGroupConfig, } from '../../../types'; -import { DragContextState } from '../../../drag_drop'; export interface ConfigPanelWrapperProps { activeDatasourceId: string; visualizationState: unknown; @@ -51,7 +50,6 @@ export interface LayerPanelProps { export interface LayerDatasourceDropProps { layerId: string; - dragDropContext: DragContextState; state: unknown; setState: (newState: unknown) => void; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index 92a2f0c5d03fc..218ceb8206080 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -6,7 +6,7 @@ */ import './chart_switch.scss'; -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, memo } from 'react'; import { EuiIcon, EuiPopover, @@ -79,7 +79,7 @@ function VisualizationSummary(props: Props) { ); } -export function ChartSwitch(props: Props) { +export const ChartSwitch = memo(function ChartSwitch(props: Props) { const [flyoutOpen, setFlyoutOpen] = useState(false); const commitSelection = (selection: VisualizationSelection) => { @@ -305,7 +305,7 @@ export function ChartSwitch(props: Props) { ); return
{popover}
; -} +}); function getTopSuggestion( props: Props, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index 48aa56efdb3cc..ab718a99843c8 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -794,6 +794,7 @@ describe('workspace_panel', () => { setKeyboardMode={() => {}} setA11yMessage={() => {}} registerDropTarget={jest.fn()} + dropTargetsByOrder={undefined} > dragDropContext.dragging && getSuggestionForField(dragDropContext.dragging), + [dragDropContext.dragging, getSuggestionForField] + ); + + return ( + + ); +}); + +// Exported for testing purposes only. +export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ activeDatasourceId, activeVisualizationId, visualizationMap, @@ -102,13 +118,10 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ ExpressionRenderer: ExpressionRendererComponent, title, visualizeTriggerFieldContext, - getSuggestionForField, -}: WorkspacePanelProps) { - const dragDropContext = useContext(DragContext); - - const suggestionForDraggedField = - dragDropContext.dragging && getSuggestionForField(dragDropContext.dragging); - + suggestionForDraggedField, +}: Omit & { + suggestionForDraggedField: Suggestion | undefined; +}) { const [localState, setLocalState] = useState({ expressionBuildError: undefined, expandError: false, @@ -173,6 +186,8 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ ] ); + const expressionExists = Boolean(expression); + const onEvent = useCallback( (event: ExpressionRendererEvent) => { if (!plugins.uiActions) { @@ -202,23 +217,23 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ useEffect(() => { // reset expression error if component attempts to run it again - if (expression && localState.expressionBuildError) { + if (expressionExists && localState.expressionBuildError) { setLocalState((s) => ({ ...s, expressionBuildError: undefined, })); } - }, [expression, localState.expressionBuildError]); + }, [expressionExists, localState.expressionBuildError]); - function onDrop() { + const onDrop = useCallback(() => { if (suggestionForDraggedField) { trackUiEvent('drop_onto_workspace'); - trackUiEvent(expression ? 'drop_non_empty' : 'drop_empty'); + trackUiEvent(expressionExists ? 'drop_non_empty' : 'drop_empty'); switchToSuggestion(dispatch, suggestionForDraggedField, 'SWITCH_VISUALIZATION'); } - } + }, [suggestionForDraggedField, expressionExists, dispatch]); - function renderEmptyWorkspace() { + const renderEmptyWorkspace = () => { return (

- {expression === null + {!expressionExists ? i18n.translate('xpack.lens.editorFrame.emptyWorkspace', { defaultMessage: 'Drop some fields here to start', }) @@ -239,7 +254,7 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({

- {expression === null && ( + {!expressionExists && ( <>

{i18n.translate('xpack.lens.editorFrame.emptyWorkspaceHeading', { @@ -263,9 +278,9 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ )} ); - } + }; - function renderVisualization() { + const renderVisualization = () => { // we don't want to render the emptyWorkspace on visualizing field from Discover // as it is specific for the drag and drop functionality and can confuse the users if (expression === null && !visualizeTriggerFieldContext) { @@ -283,7 +298,7 @@ export const WorkspacePanel = React.memo(function WorkspacePanel({ ExpressionRendererComponent={ExpressionRendererComponent} /> ); - } + }; return ( { let state: IndexPatternPrivateState; let setState: jest.Mock; let defaultProps: IndexPatternDimensionEditorProps; - let dragDropContext: DragContextState; beforeEach(() => { state = { @@ -140,8 +137,6 @@ describe('IndexPatternDimensionEditorPanel', () => { setState = jest.fn(); - dragDropContext = createMockedDragDropContext(); - defaultProps = { state, setState, @@ -174,24 +169,28 @@ describe('IndexPatternDimensionEditorPanel', () => { }); const groupId = 'a'; + describe('getDropProps', () => { it('returns undefined if no drag is happening', () => { - expect(getDropProps({ ...defaultProps, groupId, dragDropContext })).toBe(undefined); + const dragging = { + name: 'bar', + id: 'bar', + humanData: { label: 'Label' }, + }; + expect(getDropProps({ ...defaultProps, groupId, dragging })).toBe(undefined); }); it('returns undefined if the dragged item has no field', () => { + const dragging = { + name: 'bar', + id: 'bar', + humanData: { label: 'Label' }, + }; expect( getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - name: 'bar', - id: 'bar', - humanData: { label: 'Label' }, - }, - }, + dragging, }) ).toBe(undefined); }); @@ -201,14 +200,11 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - indexPatternId: 'foo', - field: { type: 'string', name: 'mystring', aggregatable: true }, - id: 'mystring', - humanData: { label: 'Label' }, - }, + dragging: { + indexPatternId: 'foo', + field: { type: 'string', name: 'mystring', aggregatable: true }, + id: 'mystring', + humanData: { label: 'Label' }, }, filterOperations: () => false, }) @@ -220,10 +216,7 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: draggingField, - }, + dragging: draggingField, filterOperations: (op: OperationMetadata) => op.dataType === 'number', }) ).toEqual({ dropType: 'field_replace', nextLabel: 'Intervals' }); @@ -234,14 +227,11 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - field: { type: 'number', name: 'bar', aggregatable: true }, - indexPatternId: 'foo2', - id: 'bar', - humanData: { label: 'Label' }, - }, + dragging: { + field: { type: 'number', name: 'bar', aggregatable: true }, + indexPatternId: 'foo2', + id: 'bar', + humanData: { label: 'Label' }, }, filterOperations: (op: OperationMetadata) => op.dataType === 'number', }) @@ -253,21 +243,18 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - field: { - name: 'timestamp', - displayName: 'timestampLabel', - type: 'date', - aggregatable: true, - searchable: true, - exists: true, - }, - indexPatternId: 'foo', - id: 'bar', - humanData: { label: 'Label' }, + dragging: { + field: { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + exists: true, }, + indexPatternId: 'foo', + id: 'bar', + humanData: { label: 'Label' }, }, }) ).toBe(undefined); @@ -278,15 +265,12 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - columnId: 'col1', - groupId: 'b', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }, + dragging: { + columnId: 'col1', + groupId: 'b', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, }, columnId: 'col2', }) @@ -321,16 +305,14 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - columnId: 'col1', - groupId: 'b', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }, + dragging: { + columnId: 'col1', + groupId: 'b', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, }, + columnId: 'col2', }) ).toEqual(undefined); @@ -360,15 +342,12 @@ describe('IndexPatternDimensionEditorPanel', () => { getDropProps({ ...defaultProps, groupId, - dragDropContext: { - ...dragDropContext, - dragging: { - columnId: 'col1', - groupId: 'b', - layerId: 'first', - id: 'col1', - humanData: { label: 'Label' }, - }, + dragging: { + columnId: 'col1', + groupId: 'b', + layerId: 'first', + id: 'col1', + humanData: { label: 'Label' }, }, columnId: 'col2', filterOperations: (op: OperationMetadata) => op.isBucketed === false, @@ -380,10 +359,6 @@ describe('IndexPatternDimensionEditorPanel', () => { it('appends the dropped column when a field is dropped', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: draggingField, - }, droppedItem: draggingField, dropType: 'field_replace', columnId: 'col2', @@ -412,10 +387,6 @@ describe('IndexPatternDimensionEditorPanel', () => { it('selects the specific operation that was valid on drop', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: draggingField, - }, droppedItem: draggingField, columnId: 'col2', filterOperations: (op: OperationMetadata) => op.isBucketed, @@ -444,10 +415,6 @@ describe('IndexPatternDimensionEditorPanel', () => { it('updates a column when a field is dropped', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: draggingField, - }, droppedItem: draggingField, filterOperations: (op: OperationMetadata) => op.dataType === 'number', dropType: 'field_replace', @@ -470,18 +437,8 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('keeps the operation when dropping a different compatible field', () => { - const dragging = { - field: { name: 'memory', type: 'number', aggregatable: true }, - indexPatternId: 'foo', - id: '1', - humanData: { label: 'Label' }, - }; onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, droppedItem: { field: { name: 'memory', type: 'number', aggregatable: true }, indexPatternId: 'foo', @@ -538,10 +495,6 @@ describe('IndexPatternDimensionEditorPanel', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, droppedItem: dragging, columnId: 'col2', dropType: 'move_compatible', @@ -598,10 +551,6 @@ describe('IndexPatternDimensionEditorPanel', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: defaultDragging, - }, droppedItem: defaultDragging, state: testState, dropType: 'replace_compatible', @@ -667,10 +616,6 @@ describe('IndexPatternDimensionEditorPanel', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: metricDragging, - }, droppedItem: metricDragging, state: testState, dropType: 'duplicate_in_group', @@ -703,10 +648,6 @@ describe('IndexPatternDimensionEditorPanel', () => { onDrop({ ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging: bucketDragging, - }, droppedItem: bucketDragging, state: testState, dropType: 'duplicate_in_group', @@ -768,10 +709,7 @@ describe('IndexPatternDimensionEditorPanel', () => { const defaultReorderDropParams = { ...defaultProps, - dragDropContext: { - ...dragDropContext, - dragging, - }, + dragging, droppedItem: dragging, state: testState, filterOperations: (op: OperationMetadata) => op.dataType === 'number', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts index be791b3c7f7ce..a7d4774d8aa3d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts @@ -23,6 +23,7 @@ import { mergeLayer } from '../state_helpers'; import { hasField, isDraggedField } from '../utils'; import { IndexPatternPrivateState, DraggedField } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; +import { DragContextState } from '../../drag_drop/providers'; type DropHandlerProps = DatasourceDimensionDropHandlerProps & { droppedItem: T; @@ -31,9 +32,12 @@ type DropHandlerProps = DatasourceDimensionDropHandlerProps & { groupId: string } + props: DatasourceDimensionDropProps & { + dragging: DragContextState['dragging']; + groupId: string; + } ): { dropType: DropType; nextLabel?: string } | undefined { - const { dragging } = props.dragDropContext; + const { dragging } = props; if (!dragging) { return; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index b8b5eb4c1e6f8..aa144c96dc7af 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -93,6 +93,21 @@ export function getIndexPatternDatasource({ const indexPatternsService = data.indexPatterns; + const handleChangeIndexPattern = ( + id: string, + state: IndexPatternPrivateState, + setState: StateSetter + ) => { + changeIndexPattern({ + id, + state, + setState, + onError: onIndexPatternLoadError, + storage, + indexPatternsService, + }); + }; + // Not stateful. State is persisted to the frame const indexPatternDatasource: Datasource = { id: 'indexpattern', @@ -171,20 +186,7 @@ export function getIndexPatternDatasource({ render( - ) => { - changeIndexPattern({ - id, - state, - setState, - onError: onIndexPatternLoadError, - storage, - indexPatternsService, - }); - }} + changeIndexPattern={handleChangeIndexPattern} data={data} charts={charts} {...props} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts index 06560bb0fa244..e71b26b9d4cd9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts @@ -253,6 +253,7 @@ export function createMockedDragDropContext(): jest.Mocked { keyboardMode: false, setKeyboardMode: jest.fn(), setA11yMessage: jest.fn(), + dropTargetsByOrder: undefined, registerDropTarget: jest.fn(), }; } diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 419354117eda2..6ac2d98994be3 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -190,7 +190,10 @@ export interface Datasource { renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps) => void; renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; getDropProps: ( - props: DatasourceDimensionDropProps & { groupId: string } + props: DatasourceDimensionDropProps & { + groupId: string; + dragging: DragContextState['dragging']; + } ) => { dropType: DropType; nextLabel?: string } | undefined; onDrop: (props: DatasourceDimensionDropHandlerProps) => false | true | { deleted: string }; updateStateOnCloseDimension?: (props: { @@ -278,9 +281,7 @@ export type DatasourceDimensionEditorProps = DatasourceDimensionPro dimensionGroups: VisualizationDimensionGroupConfig[]; }; -export type DatasourceDimensionTriggerProps = DatasourceDimensionProps & { - dragDropContext: DragContextState; -}; +export type DatasourceDimensionTriggerProps = DatasourceDimensionProps; export interface DatasourceLayerPanelProps { layerId: string; @@ -310,7 +311,6 @@ export type DatasourceDimensionDropProps = SharedDimensionProps & { columnId: string; state: T; setState: StateSetter; - dragDropContext: DragContextState; }; export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & { From 556a882a24a32e1be6c8364e5d1251d647ceaec7 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sun, 21 Feb 2021 15:03:47 +0300 Subject: [PATCH 32/32] [coverage] ingest data in parallel (#92074) --- .../generate_team_assignments_and_ingest_coverage.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh index 243dbaa6197e6..ad123eeb05095 100644 --- a/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh +++ b/src/dev/code_coverage/shell_scripts/generate_team_assignments_and_ingest_coverage.sh @@ -41,9 +41,10 @@ for x in functional jest; do # Need to override COVERAGE_INGESTION_KIBANA_ROOT since json file has original intake worker path export COVERAGE_INGESTION_KIBANA_ROOT=/dev/shm/workspace/kibana fi - - node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath $TEAM_ASSIGN_PATH + # running in background to speed up ingestion + node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt --teamAssignmentsPath $TEAM_ASSIGN_PATH & done +wait echo "### Ingesting Code Coverage - Complete" echo ""