From a6e6b62658fe9d1783720781326e5c481ff0f284 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 14 Dec 2020 17:51:18 +0100 Subject: [PATCH 01/44] [ML] Transforms: Support for missing_bucket in transform advanced pivot editor (#85758) Adds support for missing_bucket with group-by configurations in the advanced editor for pivot configurations. Previously, the editor would allow you to add the setting, but it would be stripped from the config once the transform gets created. --- .../transform/common/types/pivot_group_by.ts | 3 + .../public/app/common/pivot_group_by.ts | 3 + .../public/app/common/request.test.ts | 97 ++++++++++--------- .../transform/public/app/common/request.ts | 10 ++ 4 files changed, 67 insertions(+), 46 deletions(-) diff --git a/x-pack/plugins/transform/common/types/pivot_group_by.ts b/x-pack/plugins/transform/common/types/pivot_group_by.ts index bfaf17a32b580..3d1a833b1b562 100644 --- a/x-pack/plugins/transform/common/types/pivot_group_by.ts +++ b/x-pack/plugins/transform/common/types/pivot_group_by.ts @@ -12,6 +12,7 @@ export type GenericAgg = object; export interface TermsAgg { terms: { field: EsFieldName; + missing_bucket?: boolean; }; } @@ -19,6 +20,7 @@ export interface HistogramAgg { histogram: { field: EsFieldName; interval: string; + missing_bucket?: boolean; }; } @@ -26,6 +28,7 @@ export interface DateHistogramAgg { date_histogram: { field: EsFieldName; calendar_interval: string; + missing_bucket?: boolean; }; } diff --git a/x-pack/plugins/transform/public/app/common/pivot_group_by.ts b/x-pack/plugins/transform/public/app/common/pivot_group_by.ts index 2c2bac369c72d..281aee0805161 100644 --- a/x-pack/plugins/transform/public/app/common/pivot_group_by.ts +++ b/x-pack/plugins/transform/public/app/common/pivot_group_by.ts @@ -52,17 +52,20 @@ interface GroupByDateHistogram extends GroupByConfigBase { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM; field: EsFieldName; calendar_interval: string; + missing_bucket?: boolean; } interface GroupByHistogram extends GroupByConfigBase { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM; field: EsFieldName; interval: string; + missing_bucket?: boolean; } interface GroupByTerms extends GroupByConfigBase { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS; field: EsFieldName; + missing_bucket?: boolean; } export type GroupByConfigWithInterval = GroupByDateHistogram | GroupByHistogram; diff --git a/x-pack/plugins/transform/public/app/common/request.test.ts b/x-pack/plugins/transform/public/app/common/request.test.ts index 46ace2c3315a5..cc58308a165c8 100644 --- a/x-pack/plugins/transform/public/app/common/request.test.ts +++ b/x-pack/plugins/transform/public/app/common/request.test.ts @@ -18,6 +18,7 @@ import { getPreviewTransformRequestBody, getCreateTransformRequestBody, getCreateTransformSettingsRequestBody, + getMissingBucketConfig, getPivotQuery, isDefaultQuery, isMatchAllQuery, @@ -28,6 +29,20 @@ import { const simpleQuery: PivotQuery = { query_string: { query: 'airline:AAL' } }; +const groupByTerms: PivotGroupByConfig = { + agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, + field: 'the-group-by-field', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', +}; + +const aggsAvg: PivotAggsConfig = { + agg: PIVOT_SUPPORTED_AGGS.AVG, + field: 'the-agg-field', + aggName: 'the-agg-agg-name', + dropDownName: 'the-agg-drop-down-name', +}; + describe('Transform: Common', () => { test('isMatchAllQuery()', () => { expect(isMatchAllQuery(defaultQuery)).toBe(false); @@ -47,6 +62,16 @@ describe('Transform: Common', () => { expect(isDefaultQuery(simpleQuery)).toBe(false); }); + test('getMissingBucketConfig()', () => { + expect(getMissingBucketConfig(groupByTerms)).toEqual({}); + expect(getMissingBucketConfig({ ...groupByTerms, ...{ missing_bucket: true } })).toEqual({ + missing_bucket: true, + }); + expect(getMissingBucketConfig({ ...groupByTerms, ...{ missing_bucket: false } })).toEqual({ + missing_bucket: false, + }); + }); + test('getPivotQuery()', () => { const query = getPivotQuery('the-query'); @@ -60,22 +85,8 @@ describe('Transform: Common', () => { test('getPreviewTransformRequestBody()', () => { const query = getPivotQuery('the-query'); - const groupBy: PivotGroupByConfig[] = [ - { - agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, - field: 'the-group-by-field', - aggName: 'the-group-by-agg-name', - dropDownName: 'the-group-by-drop-down-name', - }, - ]; - const aggs: PivotAggsConfig[] = [ - { - agg: PIVOT_SUPPORTED_AGGS.AVG, - field: 'the-agg-field', - aggName: 'the-agg-agg-name', - dropDownName: 'the-agg-drop-down-name', - }, - ]; + const groupBy: PivotGroupByConfig[] = [groupByTerms]; + const aggs: PivotAggsConfig[] = [aggsAvg]; const request = getPreviewTransformRequestBody('the-index-pattern-title', query, groupBy, aggs); expect(request).toEqual({ @@ -92,22 +103,8 @@ describe('Transform: Common', () => { test('getPreviewTransformRequestBody() with comma-separated index pattern', () => { const query = getPivotQuery('the-query'); - const groupBy: PivotGroupByConfig[] = [ - { - agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, - field: 'the-group-by-field', - aggName: 'the-group-by-agg-name', - dropDownName: 'the-group-by-drop-down-name', - }, - ]; - const aggs: PivotAggsConfig[] = [ - { - agg: PIVOT_SUPPORTED_AGGS.AVG, - field: 'the-agg-field', - aggName: 'the-agg-agg-name', - dropDownName: 'the-agg-drop-down-name', - }, - ]; + const groupBy: PivotGroupByConfig[] = [groupByTerms]; + const aggs: PivotAggsConfig[] = [aggsAvg]; const request = getPreviewTransformRequestBody( 'the-index-pattern-title,the-other-title', query, @@ -127,22 +124,30 @@ describe('Transform: Common', () => { }); }); + test('getPreviewTransformRequestBody() with missing_buckets config', () => { + const query = getPivotQuery('the-query'); + const groupBy: PivotGroupByConfig[] = [{ ...groupByTerms, ...{ missing_bucket: true } }]; + const aggs: PivotAggsConfig[] = [aggsAvg]; + const request = getPreviewTransformRequestBody('the-index-pattern-title', query, groupBy, aggs); + + expect(request).toEqual({ + pivot: { + aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } }, + group_by: { + 'the-group-by-agg-name': { terms: { field: 'the-group-by-field', missing_bucket: true } }, + }, + }, + source: { + index: ['the-index-pattern-title'], + query: { query_string: { default_operator: 'AND', query: 'the-query' } }, + }, + }); + }); + test('getCreateTransformRequestBody()', () => { - const groupBy: PivotGroupByConfig = { - agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, - field: 'the-group-by-field', - aggName: 'the-group-by-agg-name', - dropDownName: 'the-group-by-drop-down-name', - }; - const agg: PivotAggsConfig = { - agg: PIVOT_SUPPORTED_AGGS.AVG, - field: 'the-agg-field', - aggName: 'the-agg-agg-name', - dropDownName: 'the-agg-drop-down-name', - }; const pivotState: StepDefineExposedState = { - aggList: { 'the-agg-name': agg }, - groupByList: { 'the-group-by-name': groupBy }, + aggList: { 'the-agg-name': aggsAvg }, + groupByList: { 'the-group-by-name': groupByTerms }, isAdvancedPivotEditorEnabled: false, isAdvancedSourceEditorEnabled: false, sourceConfigUpdated: false, diff --git a/x-pack/plugins/transform/public/app/common/request.ts b/x-pack/plugins/transform/public/app/common/request.ts index 8ee235baf7c5a..d92a4ee258757 100644 --- a/x-pack/plugins/transform/public/app/common/request.ts +++ b/x-pack/plugins/transform/public/app/common/request.ts @@ -30,6 +30,7 @@ import { isGroupByDateHistogram, isGroupByHistogram, isGroupByTerms, + GroupByConfigWithUiSupport, PivotGroupByConfig, } from '../common'; @@ -71,6 +72,12 @@ export function isDefaultQuery(query: PivotQuery): boolean { return isSimpleQuery(query) && query.query_string.query === '*'; } +export const getMissingBucketConfig = ( + g: GroupByConfigWithUiSupport +): { missing_bucket?: boolean } => { + return g.missing_bucket !== undefined ? { missing_bucket: g.missing_bucket } : {}; +}; + export function getPreviewTransformRequestBody( indexPatternTitle: IndexPattern['title'], query: PivotQuery, @@ -95,6 +102,7 @@ export function getPreviewTransformRequestBody( const termsAgg: TermsAgg = { terms: { field: g.field, + ...getMissingBucketConfig(g), }, }; request.pivot.group_by[g.aggName] = termsAgg; @@ -103,6 +111,7 @@ export function getPreviewTransformRequestBody( histogram: { field: g.field, interval: g.interval, + ...getMissingBucketConfig(g), }, }; request.pivot.group_by[g.aggName] = histogramAgg; @@ -111,6 +120,7 @@ export function getPreviewTransformRequestBody( date_histogram: { field: g.field, calendar_interval: g.calendar_interval, + ...getMissingBucketConfig(g), }, }; request.pivot.group_by[g.aggName] = dateHistogramAgg; From 8b6831a55a30e20d0845c02ae88935c43754af74 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 14 Dec 2020 10:05:59 -0700 Subject: [PATCH 02/44] [Maps] fix color-style disappears when mapping by percentiles when breaks are identical (#85654) * [Maps] fix color-style disappears when mapping by percentiles when breaks are identical * tslint Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../properties/dynamic_style_property.test.ts | 47 +++++++++++++++++++ .../properties/dynamic_style_property.tsx | 30 ++++++++---- 2 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.test.ts diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.test.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.test.ts new file mode 100644 index 0000000000000..dabd8cd4cf4ee --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { percentilesValuesToFieldMeta } from './dynamic_style_property'; + +describe('percentilesValuesToFieldMeta', () => { + test('should return null when values is not defined', () => { + expect(percentilesValuesToFieldMeta(undefined)).toBeNull(); + expect(percentilesValuesToFieldMeta({})).toBeNull(); + }); + + test('should convert values to percentiles field meta', () => { + expect(percentilesValuesToFieldMeta(undefined)).toBeNull(); + expect( + percentilesValuesToFieldMeta({ + values: { + '25.0': 375.0, + '50.0': 400.0, + '75.0': 550.0, + }, + }) + ).toEqual([ + { percentile: '25.0', value: 375.0 }, + { percentile: '50.0', value: 400.0 }, + { percentile: '75.0', value: 550.0 }, + ]); + }); + + test('should remove duplicated percentile percentilesValuesToFieldMeta', () => { + expect(percentilesValuesToFieldMeta(undefined)).toBeNull(); + expect( + percentilesValuesToFieldMeta({ + values: { + '25.0': 375.0, + '50.0': 375.0, + '75.0': 550.0, + }, + }) + ).toEqual([ + { percentile: '25.0', value: 375.0 }, + { percentile: '75.0', value: 550.0 }, + ]); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx index 2f2ddd7d539cf..882247e375ddc 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx @@ -28,6 +28,7 @@ import { import { CategoryFieldMeta, FieldMetaOptions, + PercentilesFieldMeta, RangeFieldMeta, StyleMetaData, } from '../../../../../common/descriptor_types'; @@ -144,15 +145,8 @@ export class DynamicStyleProperty const styleMetaData = styleMetaDataRequest.getData() as StyleMetaData; const percentiles = styleMetaData[`${this._field.getRootName()}_percentiles`] as | undefined - | { values?: { [key: string]: number } }; - return percentiles !== undefined && percentiles.values !== undefined - ? Object.keys(percentiles.values).map((key) => { - return { - percentile: key, - value: percentiles.values![key], - }; - }) - : null; + | PercentilesValues; + return percentilesValuesToFieldMeta(percentiles); } getCategoryFieldMeta() { @@ -499,3 +493,21 @@ export function getNumericalMbFeatureStateValue(value: RawValue) { const valueAsFloat = parseFloat(value); return isNaN(valueAsFloat) ? null : valueAsFloat; } + +interface PercentilesValues { + values?: { [key: string]: number }; +} +export function percentilesValuesToFieldMeta( + percentiles?: PercentilesValues | undefined +): PercentilesFieldMeta | null { + if (percentiles === undefined || percentiles.values === undefined) { + return null; + } + const percentilesFieldMeta = Object.keys(percentiles.values).map((key) => { + return { + percentile: key, + value: percentiles.values![key], + }; + }); + return _.uniqBy(percentilesFieldMeta, 'value'); +} From 6ae00fb5146476b9e6fe5e8ad115f59ff96f715d Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Mon, 14 Dec 2020 12:12:11 -0500 Subject: [PATCH 03/44] [Monitoring] Convert Beats-related server files that read from _source to typescript (#85193) * Beats to TS * PR feedback * Fix types * Fix types * Fix failing test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../monitoring/server/lib/apm/get_apm_info.ts | 22 ++++-- .../monitoring/server/lib/apm/get_apms.ts | 18 +++-- .../server/lib/beats/get_beat_summary.ts | 14 +++- .../lib/beats/{get_beats.js => get_beats.ts} | 77 +++++++++++++------ x-pack/plugins/monitoring/server/types.ts | 4 +- 5 files changed, 92 insertions(+), 43 deletions(-) rename x-pack/plugins/monitoring/server/lib/beats/{get_beats.js => get_beats.ts} (64%) diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.ts b/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.ts index 4ca708e9d2832..7d471d528595e 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.ts +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apm_info.ts @@ -21,17 +21,23 @@ export function handleResponse(response: ElasticsearchResponse, apmUuid: string) return {}; } - const firstStats = response.hits.hits[0].inner_hits.first_hit.hits.hits[0]._source.beats_stats; - const stats = response.hits.hits[0]._source.beats_stats; + const firstHit = response.hits.hits[0]; - if (!firstStats || !stats) { - return {}; + let firstStats = null; + const stats = firstHit._source.beats_stats ?? {}; + + if ( + firstHit.inner_hits?.first_hit?.hits?.hits && + firstHit.inner_hits?.first_hit?.hits?.hits.length > 0 && + firstHit.inner_hits.first_hit.hits.hits[0]._source.beats_stats + ) { + firstStats = firstHit.inner_hits.first_hit.hits.hits[0]._source.beats_stats; } - const eventsTotalFirst = firstStats.metrics?.libbeat?.pipeline?.events?.total; - const eventsEmittedFirst = firstStats.metrics?.libbeat?.pipeline?.events?.published; - const eventsDroppedFirst = firstStats.metrics?.libbeat?.pipeline?.events?.dropped; - const bytesWrittenFirst = firstStats.metrics?.libbeat?.output?.write?.bytes; + const eventsTotalFirst = firstStats?.metrics?.libbeat?.pipeline?.events?.total; + const eventsEmittedFirst = firstStats?.metrics?.libbeat?.pipeline?.events?.published; + const eventsDroppedFirst = firstStats?.metrics?.libbeat?.pipeline?.events?.dropped; + const bytesWrittenFirst = firstStats?.metrics?.libbeat?.output?.write?.bytes; const eventsTotalLast = stats.metrics?.libbeat?.pipeline?.events?.total; const eventsEmittedLast = stats.metrics?.libbeat?.pipeline?.events?.published; diff --git a/x-pack/plugins/monitoring/server/lib/apm/get_apms.ts b/x-pack/plugins/monitoring/server/lib/apm/get_apms.ts index f6df94f8de138..7677677ea5e75 100644 --- a/x-pack/plugins/monitoring/server/lib/apm/get_apms.ts +++ b/x-pack/plugins/monitoring/server/lib/apm/get_apms.ts @@ -24,9 +24,13 @@ export function handleResponse(response: ElasticsearchResponse, start: number, e return accum; } - const earliestStats = hit.inner_hits.earliest.hits.hits[0]._source.beats_stats; - if (!earliestStats) { - return accum; + let earliestStats = null; + if ( + hit.inner_hits?.earliest?.hits?.hits && + hit.inner_hits?.earliest?.hits?.hits.length > 0 && + hit.inner_hits.earliest.hits.hits[0]._source.beats_stats + ) { + earliestStats = hit.inner_hits.earliest.hits.hits[0]._source.beats_stats; } const uuid = stats?.beat?.uuid; @@ -41,7 +45,7 @@ export function handleResponse(response: ElasticsearchResponse, start: number, e // add the beat const rateOptions = { hitTimestamp: stats.timestamp, - earliestHitTimestamp: earliestStats.timestamp, + earliestHitTimestamp: earliestStats?.timestamp, timeWindowMin: start, timeWindowMax: end, }; @@ -54,14 +58,14 @@ export function handleResponse(response: ElasticsearchResponse, start: number, e const { rate: totalEventsRate } = calculateRate({ latestTotal: stats.metrics?.libbeat?.pipeline?.events?.total, - earliestTotal: earliestStats.metrics?.libbeat?.pipeline?.events?.total, + earliestTotal: earliestStats?.metrics?.libbeat?.pipeline?.events?.total, ...rateOptions, }); const errorsWrittenLatest = stats.metrics?.libbeat?.output?.write?.errors ?? 0; - const errorsWrittenEarliest = earliestStats.metrics?.libbeat?.output?.write?.errors ?? 0; + const errorsWrittenEarliest = earliestStats?.metrics?.libbeat?.output?.write?.errors ?? 0; const errorsReadLatest = stats.metrics?.libbeat?.output?.read?.errors ?? 0; - const errorsReadEarliest = earliestStats.metrics?.libbeat?.output?.read?.errors ?? 0; + const errorsReadEarliest = earliestStats?.metrics?.libbeat?.output?.read?.errors ?? 0; const errors = getDiffCalculation( errorsWrittenLatest + errorsReadLatest, errorsWrittenEarliest + errorsReadEarliest diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.ts b/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.ts index 57325673a131a..80b5efda4047a 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.ts +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beat_summary.ts @@ -18,8 +18,18 @@ export function handleResponse(response: ElasticsearchResponse, beatUuid: string return {}; } - const firstStats = response.hits.hits[0].inner_hits.first_hit.hits.hits[0]._source.beats_stats; - const stats = response.hits.hits[0]._source.beats_stats; + const firstHit = response.hits.hits[0]; + + let firstStats = null; + if ( + firstHit.inner_hits?.first_hit?.hits?.hits && + firstHit.inner_hits?.first_hit?.hits?.hits.length > 0 && + firstHit.inner_hits.first_hit.hits.hits[0]._source.beats_stats + ) { + firstStats = firstHit.inner_hits.first_hit.hits.hits[0]._source.beats_stats; + } + + const stats = firstHit._source.beats_stats ?? {}; const eventsTotalFirst = firstStats?.metrics?.libbeat?.pipeline?.events?.total ?? null; const eventsEmittedFirst = firstStats?.metrics?.libbeat?.pipeline?.events?.published ?? null; diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_beats.js b/x-pack/plugins/monitoring/server/lib/beats/get_beats.ts similarity index 64% rename from x-pack/plugins/monitoring/server/lib/beats/get_beats.js rename to x-pack/plugins/monitoring/server/lib/beats/get_beats.ts index af4b6c31a3e5e..beda4334b4937 100644 --- a/x-pack/plugins/monitoring/server/lib/beats/get_beats.js +++ b/x-pack/plugins/monitoring/server/lib/beats/get_beats.ts @@ -5,18 +5,39 @@ */ import moment from 'moment'; -import { upperFirst, get } from 'lodash'; +import { upperFirst } from 'lodash'; +// @ts-ignore import { checkParam } from '../error_missing_required'; +// @ts-ignore import { createBeatsQuery } from './create_beats_query'; +// @ts-ignore import { calculateRate } from '../calculate_rate'; +// @ts-ignore import { getDiffCalculation } from './_beats_stats'; +import { ElasticsearchResponse, LegacyRequest } from '../../types'; + +interface Beat { + uuid: string | undefined; + name: string | undefined; + type: string | undefined; + output: string | undefined; + total_events_rate: number; + bytes_sent_rate: number; + memory: number | undefined; + version: string | undefined; + errors: any; +} -export function handleResponse(response, start, end) { - const hits = get(response, 'hits.hits', []); - const initial = { ids: new Set(), beats: [] }; +export function handleResponse(response: ElasticsearchResponse, start: number, end: number) { + const hits = response.hits?.hits ?? []; + const initial: { ids: Set; beats: Beat[] } = { ids: new Set(), beats: [] }; const { beats } = hits.reduce((accum, hit) => { - const stats = get(hit, '_source.beats_stats'); - const uuid = get(stats, 'beat.uuid'); + const stats = hit._source.beats_stats; + const uuid = stats?.beat?.uuid; + + if (!uuid) { + return accum; + } // skip this duplicated beat, newer one was already added if (accum.ids.has(uuid)) { @@ -25,47 +46,55 @@ export function handleResponse(response, start, end) { // add another beat summary accum.ids.add(uuid); - const earliestStats = get(hit, 'inner_hits.earliest.hits.hits[0]._source.beats_stats'); + + let earliestStats = null; + if ( + hit.inner_hits?.earliest?.hits?.hits && + hit.inner_hits?.earliest?.hits?.hits.length > 0 && + hit.inner_hits.earliest.hits.hits[0]._source.beats_stats + ) { + earliestStats = hit.inner_hits.earliest.hits.hits[0]._source.beats_stats; + } // add the beat const rateOptions = { - hitTimestamp: get(stats, 'timestamp'), - earliestHitTimestamp: get(earliestStats, 'timestamp'), + hitTimestamp: stats?.timestamp, + earliestHitTimestamp: earliestStats?.timestamp, timeWindowMin: start, timeWindowMax: end, }; const { rate: bytesSentRate } = calculateRate({ - latestTotal: get(stats, 'metrics.libbeat.output.write.bytes'), - earliestTotal: get(earliestStats, 'metrics.libbeat.output.write.bytes'), + latestTotal: stats?.metrics?.libbeat?.output?.write?.bytes, + earliestTotal: earliestStats?.metrics?.libbeat?.output?.write?.bytes, ...rateOptions, }); const { rate: totalEventsRate } = calculateRate({ - latestTotal: get(stats, 'metrics.libbeat.pipeline.events.total'), - earliestTotal: get(earliestStats, 'metrics.libbeat.pipeline.events.total'), + latestTotal: stats?.metrics?.libbeat?.pipeline?.events?.total, + earliestTotal: earliestStats?.metrics?.libbeat?.pipeline?.events?.total, ...rateOptions, }); - const errorsWrittenLatest = get(stats, 'metrics.libbeat.output.write.errors'); - const errorsWrittenEarliest = get(earliestStats, 'metrics.libbeat.output.write.errors'); - const errorsReadLatest = get(stats, 'metrics.libbeat.output.read.errors'); - const errorsReadEarliest = get(earliestStats, 'metrics.libbeat.output.read.errors'); + const errorsWrittenLatest = stats?.metrics?.libbeat?.output?.write?.errors ?? 0; + const errorsWrittenEarliest = earliestStats?.metrics?.libbeat?.output?.write?.errors ?? 0; + const errorsReadLatest = stats?.metrics?.libbeat?.output?.read?.errors ?? 0; + const errorsReadEarliest = earliestStats?.metrics?.libbeat?.output?.read?.errors ?? 0; const errors = getDiffCalculation( errorsWrittenLatest + errorsReadLatest, errorsWrittenEarliest + errorsReadEarliest ); accum.beats.push({ - uuid: get(stats, 'beat.uuid'), - name: get(stats, 'beat.name'), - type: upperFirst(get(stats, 'beat.type')), - output: upperFirst(get(stats, 'metrics.libbeat.output.type')), + uuid: stats?.beat?.uuid, + name: stats?.beat?.name, + type: upperFirst(stats?.beat?.type), + output: upperFirst(stats?.metrics?.libbeat?.output?.type), total_events_rate: totalEventsRate, bytes_sent_rate: bytesSentRate, errors, - memory: get(stats, 'metrics.beat.memstats.memory_alloc'), - version: get(stats, 'beat.version'), + memory: stats?.metrics?.beat?.memstats?.memory_alloc, + version: stats?.beat?.version, }); return accum; @@ -74,7 +103,7 @@ export function handleResponse(response, start, end) { return beats; } -export async function getBeats(req, beatsIndexPattern, clusterUuid) { +export async function getBeats(req: LegacyRequest, beatsIndexPattern: string, clusterUuid: string) { checkParam(beatsIndexPattern, 'beatsIndexPattern in getBeats'); const config = req.server.config(); diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 73eea99467c59..84b331df8ba42 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -121,9 +121,9 @@ export interface ElasticsearchResponse { export interface ElasticsearchResponseHit { _source: ElasticsearchSource; - inner_hits: { + inner_hits?: { [field: string]: { - hits: { + hits?: { hits: ElasticsearchResponseHit[]; total: { value: number; From 7a5c3b482c39b1d026381df2bf4e60eddf787487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Mon, 14 Dec 2020 18:29:18 +0100 Subject: [PATCH 04/44] [APM] Fix Transaction duration distribution barchart clickarea (#84394) * [APM] select transaction distribution by clicking on the entire bucket * fixing margins and bucket click * changing annotation color * adding tooltip placement bottom * addressing pr comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../app/TransactionDetails/Distribution/index.tsx | 15 +++++++-------- .../public/hooks/use_chart_theme.tsx | 6 ++++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index 309cde4dd9f65..8ab09eccd9bdb 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -7,10 +7,9 @@ import { Axis, Chart, - ElementClickListener, - GeometryValue, HistogramBarSeries, Position, + ProjectionClickListener, RectAnnotation, ScaleType, Settings, @@ -24,11 +23,11 @@ import d3 from 'd3'; import { isEmpty } from 'lodash'; import React from 'react'; import { ValuesType } from 'utility-types'; -import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { useTheme } from '../../../../../../observability/public'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; import type { IUrlParams } from '../../../../context/url_params_context/types'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { unit } from '../../../../style/variables'; import { ChartContainer } from '../../../shared/charts/chart_container'; import { EmptyMessage } from '../../../shared/EmptyMessage'; @@ -145,10 +144,9 @@ export function TransactionDistribution({ }, }; - const onBarClick: ElementClickListener = (elements) => { - const chartPoint = elements[0][0] as GeometryValue; + const onBarClick: ProjectionClickListener = ({ x }) => { const clickedBucket = distribution?.buckets.find((bucket) => { - return bucket.key === chartPoint.x; + return bucket.key === x; }); if (clickedBucket) { onBucketClick(clickedBucket); @@ -194,10 +192,11 @@ export function TransactionDistribution({ {selectedBucket && ( diff --git a/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx b/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx index 3880dcdcde0be..d672525f1a937 100644 --- a/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx +++ b/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx @@ -14,6 +14,12 @@ export function useChartTheme() { return { ...baseChartTheme, + chartMargins: { + left: 10, + right: 10, + top: 10, + bottom: 10, + }, background: { ...baseChartTheme.background, color: 'transparent', From fde0fe52ed518714b07c741a6102c5d33066db24 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 14 Dec 2020 17:30:49 +0000 Subject: [PATCH 05/44] removed unnecessary field (#85792) --- .../sections/action_connector_form/action_type_menu.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx index 3264f22bb928f..e1955cc3db786 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx @@ -86,7 +86,6 @@ export const ActionTypeMenu = ({ selectMessage: actionTypeModel ? actionTypeModel.selectMessage : '', actionType, name: actionType.name, - typeName: id.replace('.', ''), }; }); From e7500034b0d497611a784464f4426efcdc8447c6 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 14 Dec 2020 09:33:10 -0800 Subject: [PATCH 06/44] Removes un-used test_utils directory (#85783) Signed-off-by: Tyler Smalley --- src/test_utils/jest.config.js | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 src/test_utils/jest.config.js diff --git a/src/test_utils/jest.config.js b/src/test_utils/jest.config.js deleted file mode 100644 index b7e77413598c0..0000000000000 --- a/src/test_utils/jest.config.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../..', - roots: ['/src/test_utils'], -}; From 9986aff82ec253b9551eddda80cccda34f9e3831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Mon, 14 Dec 2020 18:38:15 +0100 Subject: [PATCH 07/44] [APM] Alerting: Show preview as chart of threshold (#84080) --- .../action_menu/alerting_popover_flyout.tsx | 2 +- .../index.tsx | 0 .../alerting/chart_preview/index.tsx | 112 ++++++++++++++++ .../index.stories.tsx | 0 .../index.tsx | 45 +++++-- .../apm/public/components/alerting/fields.tsx | 2 +- .../apm/public/components/alerting/helper.ts | 17 +++ .../alerting/register_apm_alerts.ts | 8 +- .../index.tsx | 5 +- .../popover_expression}/index.tsx | 0 .../service_alert_trigger.test.tsx | 33 +++++ .../index.stories.tsx | 0 .../index.tsx | 78 +++++++++-- .../index.tsx | 4 +- .../select_anomaly_severity.test.tsx | 0 .../select_anomaly_severity.tsx | 0 .../index.tsx | 59 +++++++-- .../chart_preview/get_transaction_duration.ts | 93 +++++++++++++ .../get_transaction_error_count.ts | 63 +++++++++ .../get_transaction_error_rate.ts | 84 ++++++++++++ .../apm/server/routes/alerts/chart_preview.ts | 72 ++++++++++ .../apm/server/routes/create_apm_api.ts | 12 +- .../basic/tests/alerts/chart_preview.ts | 124 ++++++++++++++++++ .../apm_api_integration/basic/tests/index.ts | 4 + 24 files changed, 774 insertions(+), 43 deletions(-) rename x-pack/plugins/apm/public/components/alerting/{AlertingFlyout => alerting_flyout}/index.tsx (100%) create mode 100644 x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx rename x-pack/plugins/apm/public/components/alerting/{ErrorCountAlertTrigger => error_count_alert_trigger}/index.stories.tsx (100%) rename x-pack/plugins/apm/public/components/alerting/{ErrorCountAlertTrigger => error_count_alert_trigger}/index.tsx (65%) create mode 100644 x-pack/plugins/apm/public/components/alerting/helper.ts rename x-pack/plugins/apm/public/components/alerting/{ServiceAlertTrigger => service_alert_trigger}/index.tsx (92%) rename x-pack/plugins/apm/public/components/alerting/{ServiceAlertTrigger/PopoverExpression => service_alert_trigger/popover_expression}/index.tsx (100%) create mode 100644 x-pack/plugins/apm/public/components/alerting/service_alert_trigger/service_alert_trigger.test.tsx rename x-pack/plugins/apm/public/components/alerting/{TransactionDurationAlertTrigger => transaction_duration_alert_trigger}/index.stories.tsx (100%) rename x-pack/plugins/apm/public/components/alerting/{TransactionDurationAlertTrigger => transaction_duration_alert_trigger}/index.tsx (70%) rename x-pack/plugins/apm/public/components/alerting/{TransactionDurationAnomalyAlertTrigger => transaction_duration_anomaly_alert_trigger}/index.tsx (96%) rename x-pack/plugins/apm/public/components/alerting/{TransactionDurationAnomalyAlertTrigger => transaction_duration_anomaly_alert_trigger}/select_anomaly_severity.test.tsx (100%) rename x-pack/plugins/apm/public/components/alerting/{TransactionDurationAnomalyAlertTrigger => transaction_duration_anomaly_alert_trigger}/select_anomaly_severity.tsx (100%) rename x-pack/plugins/apm/public/components/alerting/{TransactionErrorRateAlertTrigger => transaction_error_rate_alert_trigger}/index.tsx (71%) create mode 100644 x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts create mode 100644 x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts create mode 100644 x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts create mode 100644 x-pack/plugins/apm/server/routes/alerts/chart_preview.ts create mode 100644 x-pack/test/apm_api_integration/basic/tests/alerts/chart_preview.ts diff --git a/x-pack/plugins/apm/public/application/action_menu/alerting_popover_flyout.tsx b/x-pack/plugins/apm/public/application/action_menu/alerting_popover_flyout.tsx index 394b4caea3e7b..395233735a9d5 100644 --- a/x-pack/plugins/apm/public/application/action_menu/alerting_popover_flyout.tsx +++ b/x-pack/plugins/apm/public/application/action_menu/alerting_popover_flyout.tsx @@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { IBasePath } from '../../../../../../src/core/public'; import { AlertType } from '../../../common/alert_types'; -import { AlertingFlyout } from '../../components/alerting/AlertingFlyout'; +import { AlertingFlyout } from '../../components/alerting/alerting_flyout'; const alertLabel = i18n.translate('xpack.apm.home.alertsMenu.alerts', { defaultMessage: 'Alerts', diff --git a/x-pack/plugins/apm/public/components/alerting/AlertingFlyout/index.tsx b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/alerting/AlertingFlyout/index.tsx rename to x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx diff --git a/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx b/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx new file mode 100644 index 0000000000000..1ed5748cd757e --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + AnnotationDomainTypes, + Axis, + BarSeries, + Chart, + LineAnnotation, + niceTimeFormatter, + Position, + RectAnnotation, + RectAnnotationDatum, + ScaleType, + Settings, + TickFormatter, +} from '@elastic/charts'; +import { EuiSpacer } from '@elastic/eui'; +import React from 'react'; +import { Coordinate } from '../../../../typings/timeseries'; +import { useTheme } from '../../../hooks/use_theme'; + +interface ChartPreviewProps { + yTickFormat?: TickFormatter; + data?: Coordinate[]; + threshold: number; +} + +export function ChartPreview({ + data = [], + yTickFormat, + threshold, +}: ChartPreviewProps) { + const theme = useTheme(); + const thresholdOpacity = 0.3; + const timestamps = data.map((d) => d.x); + const xMin = Math.min(...timestamps); + const xMax = Math.max(...timestamps); + const xFormatter = niceTimeFormatter([xMin, xMax]); + + // Make the maximum Y value either the actual max or 20% more than the threshold + const values = data.map((d) => d.y ?? 0); + const yMax = Math.max(...values, threshold * 1.2); + + const style = { + fill: theme.eui.euiColorVis9, + line: { + strokeWidth: 2, + stroke: theme.eui.euiColorVis9, + opacity: 1, + }, + opacity: thresholdOpacity, + }; + + const rectDataValues: RectAnnotationDatum[] = [ + { + coordinates: { + x0: null, + x1: null, + y0: threshold, + y1: null, + }, + }, + ]; + + return ( + <> + + + + + + + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.stories.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx rename to x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.stories.tsx diff --git a/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx similarity index 65% rename from x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx rename to x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx index efa792ff44273..cce973f8587da 100644 --- a/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx @@ -8,12 +8,17 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { useParams } from 'react-router-dom'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; -import { ALERT_TYPES_CONFIG, AlertType } from '../../../../common/alert_types'; +import { AlertType, ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; -import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; +import { asInteger } from '../../../../common/utils/formatters'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { EnvironmentField, ServiceField, IsAboveField } from '../fields'; -import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; +import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; +import { useFetcher } from '../../../hooks/use_fetcher'; +import { callApmApi } from '../../../services/rest/createCallApmApi'; +import { ChartPreview } from '../chart_preview'; +import { EnvironmentField, IsAboveField, ServiceField } from '../fields'; +import { getAbsoluteTimeRange } from '../helper'; +import { ServiceAlertTrigger } from '../service_alert_trigger'; export interface AlertParams { windowSize: number; @@ -40,6 +45,23 @@ export function ErrorCountAlertTrigger(props: Props) { end, }); + const { threshold, windowSize, windowUnit, environment } = alertParams; + + const { data } = useFetcher(() => { + if (windowSize && windowUnit) { + return callApmApi({ + endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_count', + params: { + query: { + ...getAbsoluteTimeRange(windowSize, windowUnit), + environment, + serviceName, + }, + }, + }); + } + }, [windowSize, windowUnit, environment, serviceName]); + const defaults = { threshold: 25, windowSize: 1, @@ -64,14 +86,14 @@ export function ErrorCountAlertTrigger(props: Props) { unit={i18n.translate('xpack.apm.errorCountAlertTrigger.errors', { defaultMessage: ' errors', })} - onChange={(value) => setAlertParams('threshold', value)} + onChange={(value) => setAlertParams('threshold', value || 0)} />, - setAlertParams('windowSize', windowSize || '') + onChangeWindowSize={(timeWindowSize) => + setAlertParams('windowSize', timeWindowSize || '') } - onChangeWindowUnit={(windowUnit) => - setAlertParams('windowUnit', windowUnit) + onChangeWindowUnit={(timeWindowUnit) => + setAlertParams('windowUnit', timeWindowUnit) } timeWindowSize={params.windowSize} timeWindowUnit={params.windowUnit} @@ -82,6 +104,10 @@ export function ErrorCountAlertTrigger(props: Props) { />, ]; + const chartPreview = ( + + ); + return ( ); } diff --git a/x-pack/plugins/apm/public/components/alerting/fields.tsx b/x-pack/plugins/apm/public/components/alerting/fields.tsx index 858604d2baa2a..9e814bb1b58c5 100644 --- a/x-pack/plugins/apm/public/components/alerting/fields.tsx +++ b/x-pack/plugins/apm/public/components/alerting/fields.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSelectOption } from '@elastic/eui'; import { getEnvironmentLabel } from '../../../common/environment_filter_values'; -import { PopoverExpression } from './ServiceAlertTrigger/PopoverExpression'; +import { PopoverExpression } from './service_alert_trigger/popover_expression'; const ALL_OPTION = i18n.translate('xpack.apm.alerting.fields.all_option', { defaultMessage: 'All', diff --git a/x-pack/plugins/apm/public/components/alerting/helper.ts b/x-pack/plugins/apm/public/components/alerting/helper.ts new file mode 100644 index 0000000000000..fd3aebc7495a1 --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/helper.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import datemath from '@elastic/datemath'; + +export function getAbsoluteTimeRange(windowSize: number, windowUnit: string) { + const now = new Date().toISOString(); + + return { + start: + datemath.parse(`now-${windowSize}${windowUnit}`)?.toISOString() ?? now, + end: now, + }; +} diff --git a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts index 988e335af5b7c..6dc2cb3163b1f 100644 --- a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts +++ b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts @@ -25,7 +25,7 @@ export function registerApmAlerts( documentationUrl(docLinks) { return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/apm-alerts.html`; }, - alertParamsExpression: lazy(() => import('./ErrorCountAlertTrigger')), + alertParamsExpression: lazy(() => import('./error_count_alert_trigger')), validate: () => ({ errors: [], }), @@ -60,7 +60,7 @@ export function registerApmAlerts( return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/apm-alerts.html`; }, alertParamsExpression: lazy( - () => import('./TransactionDurationAlertTrigger') + () => import('./transaction_duration_alert_trigger') ), validate: () => ({ errors: [], @@ -97,7 +97,7 @@ export function registerApmAlerts( return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/apm-alerts.html`; }, alertParamsExpression: lazy( - () => import('./TransactionErrorRateAlertTrigger') + () => import('./transaction_error_rate_alert_trigger') ), validate: () => ({ errors: [], @@ -134,7 +134,7 @@ export function registerApmAlerts( return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/apm-alerts.html`; }, alertParamsExpression: lazy( - () => import('./TransactionDurationAnomalyAlertTrigger') + () => import('./transaction_duration_anomaly_alert_trigger') ), validate: () => ({ errors: [], diff --git a/x-pack/plugins/apm/public/components/alerting/ServiceAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/index.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/alerting/ServiceAlertTrigger/index.tsx rename to x-pack/plugins/apm/public/components/alerting/service_alert_trigger/index.tsx index b4d3e8f3ad241..0a12f79bf61a9 100644 --- a/x-pack/plugins/apm/public/components/alerting/ServiceAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/index.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiFlexGrid, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React, { useEffect } from 'react'; -import { EuiSpacer, EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; import { useParams } from 'react-router-dom'; interface Props { @@ -14,6 +14,7 @@ interface Props { setAlertProperty: (key: string, value: any) => void; defaults: Record; fields: React.ReactNode[]; + chartPreview?: React.ReactNode; } export function ServiceAlertTrigger(props: Props) { @@ -25,6 +26,7 @@ export function ServiceAlertTrigger(props: Props) { setAlertProperty, alertTypeName, defaults, + chartPreview, } = props; const params: Record = { @@ -61,6 +63,7 @@ export function ServiceAlertTrigger(props: Props) { ))} + {chartPreview} ); diff --git a/x-pack/plugins/apm/public/components/alerting/ServiceAlertTrigger/PopoverExpression/index.tsx b/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/popover_expression/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/alerting/ServiceAlertTrigger/PopoverExpression/index.tsx rename to x-pack/plugins/apm/public/components/alerting/service_alert_trigger/popover_expression/index.tsx diff --git a/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/service_alert_trigger.test.tsx b/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/service_alert_trigger.test.tsx new file mode 100644 index 0000000000000..72611043bbed3 --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/service_alert_trigger.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { render } from '@testing-library/react'; +import React, { ReactNode } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { ServiceAlertTrigger } from './'; + +function Wrapper({ children }: { children?: ReactNode }) { + return {children}; +} + +describe('ServiceAlertTrigger', () => { + it('renders', () => { + expect(() => + render( + {}} + setAlertProperty={() => {}} + />, + { + wrapper: Wrapper, + } + ) + ).not.toThrowError(); + }); +}); diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.stories.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.stories.tsx rename to x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.stories.tsx diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx similarity index 70% rename from x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx rename to x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx index 3566850aa24c4..f18e407cc58dd 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx @@ -4,24 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ import { EuiSelect } from '@elastic/eui'; -import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { map } from 'lodash'; import React from 'react'; +import { useParams } from 'react-router-dom'; +import { useFetcher } from '../../../../../observability/public'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; -import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; -import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; +import { TimeSeries } from '../../../../typings/timeseries'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; +import { callApmApi } from '../../../services/rest/createCallApmApi'; +import { getResponseTimeTickFormatter } from '../../shared/charts/transaction_charts/helper'; +import { useFormatter } from '../../shared/charts/transaction_charts/use_formatter'; +import { ChartPreview } from '../chart_preview'; import { EnvironmentField, + IsAboveField, ServiceField, TransactionTypeField, - IsAboveField, } from '../fields'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { getAbsoluteTimeRange } from '../helper'; +import { ServiceAlertTrigger } from '../service_alert_trigger'; +import { PopoverExpression } from '../service_alert_trigger/popover_expression'; interface AlertParams { windowSize: number; @@ -63,14 +70,58 @@ interface Props { export function TransactionDurationAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; const { urlParams } = useUrlParams(); - const { transactionTypes } = useApmServiceContext(); + const { transactionTypes, transactionType } = useApmServiceContext(); const { serviceName } = useParams<{ serviceName?: string }>(); - const { start, end, transactionType } = urlParams; + const { start, end } = urlParams; const { environmentOptions } = useEnvironmentsFetcher({ serviceName, start, end, }); + const { + aggregationType, + environment, + threshold, + windowSize, + windowUnit, + } = alertParams; + + const { data } = useFetcher(() => { + if (windowSize && windowUnit) { + return callApmApi({ + endpoint: 'GET /api/apm/alerts/chart_preview/transaction_duration', + params: { + query: { + ...getAbsoluteTimeRange(windowSize, windowUnit), + aggregationType, + environment, + serviceName, + transactionType: alertParams.transactionType, + }, + }, + }); + } + }, [ + aggregationType, + environment, + serviceName, + alertParams.transactionType, + windowSize, + windowUnit, + ]); + + const { formatter } = useFormatter([{ data: data ?? [] } as TimeSeries]); + const yTickFormat = getResponseTimeTickFormatter(formatter); + // The threshold from the form is in ms. Convert to µs. + const thresholdMs = threshold * 1000; + + const chartPreview = ( + + ); if (!transactionTypes.length || !serviceName) { return null; @@ -81,9 +132,7 @@ export function TransactionDurationAlertTrigger(props: Props) { aggregationType: 'avg', windowSize: 5, windowUnit: 'm', - - // use the current transaction type or default to the first in the list - transactionType: transactionType || transactionTypes[0], + transactionType, environment: urlParams.environment || ENVIRONMENT_ALL.value, }; @@ -127,7 +176,7 @@ export function TransactionDurationAlertTrigger(props: Props) { unit={i18n.translate('xpack.apm.transactionDurationAlertTrigger.ms', { defaultMessage: 'ms', })} - onChange={(value) => setAlertParams('threshold', value)} + onChange={(value) => setAlertParams('threshold', value || 0)} />, @@ -148,8 +197,9 @@ export function TransactionDurationAlertTrigger(props: Props) { return ( diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx rename to x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx index ff5939c601375..10c4bbff08396 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx @@ -11,8 +11,8 @@ import { ANOMALY_SEVERITY } from '../../../../../ml/common'; import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; -import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; +import { ServiceAlertTrigger } from '../service_alert_trigger'; +import { PopoverExpression } from '../service_alert_trigger/popover_expression'; import { AnomalySeverity, SelectAnomalySeverity, diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.test.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.test.tsx rename to x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.test.tsx diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.tsx rename to x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.tsx diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx similarity index 71% rename from x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx rename to x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx index f723febde389d..9707df9e86335 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx @@ -3,22 +3,26 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { useParams } from 'react-router-dom'; import React from 'react'; +import { useParams } from 'react-router-dom'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; -import { ALERT_TYPES_CONFIG, AlertType } from '../../../../common/alert_types'; -import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; - +import { AlertType, ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; +import { asPercent } from '../../../../common/utils/formatters'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; +import { useFetcher } from '../../../hooks/use_fetcher'; +import { callApmApi } from '../../../services/rest/createCallApmApi'; +import { ChartPreview } from '../chart_preview'; import { - ServiceField, - TransactionTypeField, EnvironmentField, IsAboveField, + ServiceField, + TransactionTypeField, } from '../fields'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { getAbsoluteTimeRange } from '../helper'; +import { ServiceAlertTrigger } from '../service_alert_trigger'; interface AlertParams { windowSize: number; @@ -47,6 +51,32 @@ export function TransactionErrorRateAlertTrigger(props: Props) { end, }); + const { threshold, windowSize, windowUnit, environment } = alertParams; + + const thresholdAsPercent = (threshold ?? 0) / 100; + + const { data } = useFetcher(() => { + if (windowSize && windowUnit) { + return callApmApi({ + endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_rate', + params: { + query: { + ...getAbsoluteTimeRange(windowSize, windowUnit), + environment, + serviceName, + transactionType: alertParams.transactionType, + }, + }, + }); + } + }, [ + alertParams.transactionType, + environment, + serviceName, + windowSize, + windowUnit, + ]); + if (serviceName && !transactionTypes.length) { return null; } @@ -79,7 +109,7 @@ export function TransactionErrorRateAlertTrigger(props: Props) { setAlertParams('threshold', value)} + onChange={(value) => setAlertParams('threshold', value || 0)} />, @@ -97,6 +127,14 @@ export function TransactionErrorRateAlertTrigger(props: Props) { />, ]; + const chartPreview = ( + asPercent(d, 1)} + threshold={thresholdAsPercent} + /> + ); + return ( ); } diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts new file mode 100644 index 0000000000000..37e3a2f201fb9 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MetricsAggregationResponsePart } from '../../../../../../typings/elasticsearch/aggregations'; +import { + PROCESSOR_EVENT, + SERVICE_NAME, + TRANSACTION_DURATION, + TRANSACTION_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { AlertParams } from '../../../routes/alerts/chart_preview'; +import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; + +export async function getTransactionDurationChartPreview({ + alertParams, + setup, +}: { + alertParams: AlertParams; + setup: Setup & SetupTimeRange; +}) { + const { apmEventClient, start, end } = setup; + const { + aggregationType, + environment, + serviceName, + transactionType, + } = alertParams; + + const query = { + bool: { + filter: [ + { range: rangeFilter(start, end) }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, + ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []), + ...(transactionType + ? [{ term: { [TRANSACTION_TYPE]: transactionType } }] + : []), + ...getEnvironmentUiFilterES(environment), + ], + }, + }; + + const { intervalString } = getBucketSize({ start, end, numBuckets: 20 }); + + const aggs = { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + }, + aggs: { + agg: + aggregationType === 'avg' + ? { avg: { field: TRANSACTION_DURATION } } + : { + percentiles: { + field: TRANSACTION_DURATION, + percents: [aggregationType === '95th' ? 95 : 99], + }, + }, + }, + }, + }; + const params = { + apm: { events: [ProcessorEvent.transaction] }, + body: { size: 0, query, aggs }, + }; + const resp = await apmEventClient.search(params); + + if (!resp.aggregations) { + return []; + } + + return resp.aggregations.timeseries.buckets.map((bucket) => { + const percentilesKey = aggregationType === '95th' ? '95.0' : '99.0'; + const x = bucket.key; + const y = + aggregationType === 'avg' + ? (bucket.agg as MetricsAggregationResponsePart).value + : (bucket.agg as { values: Record }).values[ + percentilesKey + ]; + + return { x, y }; + }); +} diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts new file mode 100644 index 0000000000000..28316298aeaad --- /dev/null +++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { AlertParams } from '../../../routes/alerts/chart_preview'; +import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; + +export async function getTransactionErrorCountChartPreview({ + setup, + alertParams, +}: { + setup: Setup & SetupTimeRange; + alertParams: AlertParams; +}) { + const { apmEventClient, start, end } = setup; + const { serviceName, environment } = alertParams; + + const query = { + bool: { + filter: [ + { range: rangeFilter(start, end) }, + ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []), + ...getEnvironmentUiFilterES(environment), + ], + }, + }; + + const { intervalString } = getBucketSize({ start, end, numBuckets: 20 }); + + const aggs = { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + }, + }, + }; + + const params = { + apm: { events: [ProcessorEvent.error] }, + body: { size: 0, query, aggs }, + }; + + const resp = await apmEventClient.search(params); + + if (!resp.aggregations) { + return []; + } + + return resp.aggregations.timeseries.buckets.map((bucket) => { + return { + x: bucket.key, + y: bucket.doc_count, + }; + }); +} diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts new file mode 100644 index 0000000000000..fae43ef148cfa --- /dev/null +++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + PROCESSOR_EVENT, + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { AlertParams } from '../../../routes/alerts/chart_preview'; +import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { + calculateTransactionErrorPercentage, + getOutcomeAggregation, +} from '../../helpers/transaction_error_rate'; + +export async function getTransactionErrorRateChartPreview({ + setup, + alertParams, +}: { + setup: Setup & SetupTimeRange; + alertParams: AlertParams; +}) { + const { apmEventClient, start, end } = setup; + const { serviceName, environment, transactionType } = alertParams; + + const query = { + bool: { + filter: [ + { range: rangeFilter(start, end) }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, + ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []), + ...(transactionType + ? [{ term: { [TRANSACTION_TYPE]: transactionType } }] + : []), + ...getEnvironmentUiFilterES(environment), + ], + }, + }; + + const outcomes = getOutcomeAggregation({ + searchAggregatedTransactions: false, + }); + + const { intervalString } = getBucketSize({ start, end, numBuckets: 20 }); + + const aggs = { + outcomes, + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + }, + aggs: { outcomes }, + }, + }; + + const params = { + apm: { events: [ProcessorEvent.transaction] }, + body: { size: 0, query, aggs }, + }; + + const resp = await apmEventClient.search(params); + + if (!resp.aggregations) { + return []; + } + + return resp.aggregations.timeseries.buckets.map((bucket) => { + const errorPercentage = calculateTransactionErrorPercentage( + bucket.outcomes + ); + return { + x: bucket.key, + y: errorPercentage, + }; + }); +} diff --git a/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts b/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts new file mode 100644 index 0000000000000..dc8bf45de091b --- /dev/null +++ b/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as t from 'io-ts'; +import { getTransactionDurationChartPreview } from '../../lib/alerts/chart_preview/get_transaction_duration'; +import { getTransactionErrorCountChartPreview } from '../../lib/alerts/chart_preview/get_transaction_error_count'; +import { getTransactionErrorRateChartPreview } from '../../lib/alerts/chart_preview/get_transaction_error_rate'; +import { setupRequest } from '../../lib/helpers/setup_request'; +import { createRoute } from '../create_route'; +import { rangeRt } from '../default_api_types'; + +const alertParamsRt = t.intersection([ + t.partial({ + aggregationType: t.union([ + t.literal('avg'), + t.literal('95th'), + t.literal('99th'), + ]), + serviceName: t.string, + environment: t.string, + transactionType: t.string, + }), + rangeRt, +]); + +export type AlertParams = t.TypeOf; + +export const transactionErrorRateChartPreview = createRoute({ + endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_rate', + params: t.type({ query: alertParamsRt }), + options: { tags: ['access:apm'] }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { _debug, ...alertParams } = context.params.query; + + return getTransactionErrorRateChartPreview({ + setup, + alertParams, + }); + }, +}); + +export const transactionErrorCountChartPreview = createRoute({ + endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_count', + params: t.type({ query: alertParamsRt }), + options: { tags: ['access:apm'] }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { _debug, ...alertParams } = context.params.query; + return getTransactionErrorCountChartPreview({ + setup, + alertParams, + }); + }, +}); + +export const transactionDurationChartPreview = createRoute({ + endpoint: 'GET /api/apm/alerts/chart_preview/transaction_duration', + params: t.type({ query: alertParamsRt }), + options: { tags: ['access:apm'] }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { _debug, ...alertParams } = context.params.query; + + return getTransactionDurationChartPreview({ + alertParams, + setup, + }); + }, +}); 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 d34e67083b037..b09175a6841f8 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -101,6 +101,11 @@ import { rumVisitorsBreakdownRoute, rumWebCoreVitals, } from './rum_client'; +import { + transactionErrorRateChartPreview, + transactionErrorCountChartPreview, + transactionDurationChartPreview, +} from './alerts/chart_preview'; const createApmApi = () => { const api = createApi() @@ -204,7 +209,12 @@ const createApmApi = () => { .add(rumJSErrors) .add(rumUrlSearch) .add(rumLongTaskMetrics) - .add(rumHasDataRoute); + .add(rumHasDataRoute) + + // Alerting + .add(transactionErrorCountChartPreview) + .add(transactionDurationChartPreview) + .add(transactionErrorRateChartPreview); return api; }; diff --git a/x-pack/test/apm_api_integration/basic/tests/alerts/chart_preview.ts b/x-pack/test/apm_api_integration/basic/tests/alerts/chart_preview.ts new file mode 100644 index 0000000000000..3119de47a8635 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/alerts/chart_preview.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { format } from 'url'; +import archives from '../../../common/archives_metadata'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + const archiveName = 'apm_8.0.0'; + const { end } = archives[archiveName]; + const start = new Date(Date.parse(end) - 600000).toISOString(); + + describe('Alerting chart previews', () => { + describe('GET /api/apm/alerts/chart_preview/transaction_error_rate', () => { + const url = format({ + pathname: '/api/apm/alerts/chart_preview/transaction_error_rate', + query: { + start, + end, + transactionType: 'request', + serviceName: 'opbeans-java', + }, + }); + + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + expect(response.body).to.eql([]); + }); + }); + + describe('when data is loaded', () => { + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); + + it('returns the correct data', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + expect( + response.body.some((item: { x: number; y: number | null }) => item.x && item.y) + ).to.equal(true); + }); + }); + }); + + describe('GET /api/apm/alerts/chart_preview/transaction_error_count', () => { + const url = format({ + pathname: '/api/apm/alerts/chart_preview/transaction_error_count', + query: { + start, + end, + serviceName: 'opbeans-java', + }, + }); + + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + expect(response.body).to.eql([]); + }); + }); + + describe('when data is loaded', () => { + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); + + it('returns the correct data', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + expect( + response.body.some((item: { x: number; y: number | null }) => item.x && item.y) + ).to.equal(true); + }); + }); + }); + + describe('GET /api/apm/alerts/chart_preview/transaction_duration', () => { + const url = format({ + pathname: '/api/apm/alerts/chart_preview/transaction_duration', + query: { + start, + end, + serviceName: 'opbeans-java', + transactionType: 'request', + }, + }); + + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + expect(response.body).to.eql([]); + }); + }); + + describe('when data is loaded', () => { + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); + + it('returns the correct data', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + expect( + response.body.some((item: { x: number; y: number | null }) => item.x && item.y) + ).to.equal(true); + }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/basic/tests/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts index 3e625688e2459..c0156d92439f0 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -11,6 +11,10 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont loadTestFile(require.resolve('./feature_controls')); + describe('Alerts', function () { + loadTestFile(require.resolve('./alerts/chart_preview')); + }); + describe('Service Maps', function () { loadTestFile(require.resolve('./service_maps/service_maps')); }); From 5304e88c3e6b98711418a06ff670bd9efcd41f2a Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Mon, 14 Dec 2020 18:43:25 +0100 Subject: [PATCH 08/44] [Lens] Fix chart twitching on flyout open (#85430) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/lens/public/pie_visualization/visualization.scss | 3 +++ x-pack/plugins/lens/public/xy_visualization/expression.scss | 3 +++ 2 files changed, 6 insertions(+) diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.scss b/x-pack/plugins/lens/public/pie_visualization/visualization.scss index d9ff75d849708..a8890208596b6 100644 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.scss +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.scss @@ -1,4 +1,7 @@ .lnsPieExpression__container { height: 100%; width: 100%; + // the FocusTrap is adding extra divs which are making the visualization redraw twice + // with a visible glitch. This make the chart library resilient to this extra reflow + overflow-x: hidden; } diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.scss b/x-pack/plugins/lens/public/xy_visualization/expression.scss index 579f66f99b9fb..68f5e9863d2bb 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.scss +++ b/x-pack/plugins/lens/public/xy_visualization/expression.scss @@ -1,6 +1,9 @@ .lnsXyExpression__container { height: 100%; width: 100%; + // the FocusTrap is adding extra divs which are making the visualization redraw twice + // with a visible glitch. This make the chart library resilient to this extra reflow + overflow-x: hidden; } .lnsChart__empty { From 12c40f7906c7ac0debff36920f0ba9f85db2030b Mon Sep 17 00:00:00 2001 From: Andrea Del Rio Date: Mon, 14 Dec 2020 13:03:16 -0500 Subject: [PATCH 09/44] Remove feature_directory directory and link (#84464) --- .../components/feature_directory.js | 164 ------------------ .../public/application/components/home_app.js | 4 - .../overview_page_footer.test.tsx.snap | 42 ++--- .../overview_page_footer.test.tsx | 2 +- .../overview_page_footer.tsx | 28 +-- .../translations/translations/ja-JP.json | 6 - .../translations/translations/zh-CN.json | 6 - 7 files changed, 17 insertions(+), 235 deletions(-) delete mode 100644 src/plugins/home/public/application/components/feature_directory.js diff --git a/src/plugins/home/public/application/components/feature_directory.js b/src/plugins/home/public/application/components/feature_directory.js deleted file mode 100644 index 36ececcdfd8df..0000000000000 --- a/src/plugins/home/public/application/components/feature_directory.js +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { Synopsis } from './synopsis'; -import { - EuiTabs, - EuiTab, - EuiFlexItem, - EuiFlexGrid, - EuiPage, - EuiPageBody, - EuiTitle, - EuiSpacer, -} from '@elastic/eui'; - -import { FeatureCatalogueCategory } from '../../services'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { createAppNavigationHandler } from './app_navigation_handler'; - -const ALL_TAB_ID = 'all'; -const OTHERS_TAB_ID = 'others'; - -const isOtherCategory = (directory) => { - return ( - directory.category !== FeatureCatalogueCategory.DATA && - directory.category !== FeatureCatalogueCategory.ADMIN - ); -}; - -export class FeatureDirectory extends React.Component { - constructor(props) { - super(props); - - this.tabs = [ - { - id: ALL_TAB_ID, - name: i18n.translate('home.directory.tabs.allTitle', { defaultMessage: 'All' }), - }, - { - id: FeatureCatalogueCategory.DATA, - name: i18n.translate('home.directory.tabs.dataTitle', { - defaultMessage: 'Data Exploration & Visualization', - }), - }, - { - id: FeatureCatalogueCategory.ADMIN, - name: i18n.translate('home.directory.tabs.administrativeTitle', { - defaultMessage: 'Administrative', - }), - }, - ]; - if (props.directories.some(isOtherCategory)) { - this.tabs.push({ - id: OTHERS_TAB_ID, - name: i18n.translate('home.directory.tabs.otherTitle', { defaultMessage: 'Other' }), - }); - } - - this.state = { - selectedTabId: ALL_TAB_ID, - }; - } - - onSelectedTabChanged = (id) => { - this.setState({ - selectedTabId: id, - }); - }; - - renderTabs = () => { - return this.tabs.map((tab, index) => ( - this.onSelectedTabChanged(tab.id)} - isSelected={tab.id === this.state.selectedTabId} - key={index} - > - {tab.name} - - )); - }; - - renderDirectories = () => { - return this.props.directories - .filter((directory) => { - if (this.state.selectedTabId === ALL_TAB_ID) { - return true; - } - if (this.state.selectedTabId === OTHERS_TAB_ID) { - return isOtherCategory(directory); - } - return this.state.selectedTabId === directory.category; - }) - .map((directory) => { - return ( - - - - ); - }); - }; - - render() { - return ( - - - -

- -

-
- - {this.renderTabs()} - - {this.renderDirectories()} -
-
- ); - } -} - -FeatureDirectory.propTypes = { - addBasePath: PropTypes.func.isRequired, - directories: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - icon: PropTypes.string.isRequired, - path: PropTypes.string.isRequired, - showOnHomePage: PropTypes.bool.isRequired, - category: PropTypes.string.isRequired, - order: PropTypes.number, - }) - ), -}; diff --git a/src/plugins/home/public/application/components/home_app.js b/src/plugins/home/public/application/components/home_app.js index 734100fe584ab..2ea96ad904b21 100644 --- a/src/plugins/home/public/application/components/home_app.js +++ b/src/plugins/home/public/application/components/home_app.js @@ -21,7 +21,6 @@ import React from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import PropTypes from 'prop-types'; import { Home } from './home'; -import { FeatureDirectory } from './feature_directory'; import { TutorialDirectory } from './tutorial_directory'; import { Tutorial } from './tutorial/tutorial'; import { HashRouter as Router, Switch, Route } from 'react-router-dom'; @@ -78,9 +77,6 @@ export function HomeApp({ directories, solutions }) { - - - - -
-
- - - - - + +
diff --git a/src/plugins/kibana_react/public/overview_page/overview_page_footer/overview_page_footer.test.tsx b/src/plugins/kibana_react/public/overview_page/overview_page_footer/overview_page_footer.test.tsx index f90ecdda93242..568677ee389fa 100644 --- a/src/plugins/kibana_react/public/overview_page/overview_page_footer/overview_page_footer.test.tsx +++ b/src/plugins/kibana_react/public/overview_page/overview_page_footer/overview_page_footer.test.tsx @@ -28,7 +28,7 @@ jest.mock('../../app_links', () => ({ jest.mock('../../context', () => ({ useKibana: jest.fn().mockReturnValue({ services: { - application: { capabilities: { advancedSettings: { show: true } } }, + application: { capabilities: { advancedSettings: { show: true, save: true } } }, notifications: { toast: { addSuccess: jest.fn() } }, }, }), diff --git a/src/plugins/kibana_react/public/overview_page/overview_page_footer/overview_page_footer.tsx b/src/plugins/kibana_react/public/overview_page/overview_page_footer/overview_page_footer.tsx index 113992099aee1..576046092d512 100644 --- a/src/plugins/kibana_react/public/overview_page/overview_page_footer/overview_page_footer.tsx +++ b/src/plugins/kibana_react/public/overview_page/overview_page_footer/overview_page_footer.tsx @@ -32,7 +32,7 @@ interface Props { path: string; /** Callback function to invoke when the user wants to set their default route to the current page */ onSetDefaultRoute?: (event: MouseEvent) => void; - /** Callback function to invoke when the user wants to change their default route button is changed */ + /** Callback function to invoke when the user wants to change their default route button is changed */ onChangeDefaultRoute?: (event: MouseEvent) => void; } @@ -51,9 +51,9 @@ export const OverviewPageFooter: FC = ({ } = useKibana(); const { show, save } = application.capabilities.advancedSettings; - const isAdvancedSettingsEnabled = show && save; + if (!show && !save) return <>; - const defaultRoutebutton = defaultRoute.includes(path) ? ( + const defaultRouteButton = defaultRoute.includes(path) ? ( = ({
-
{isAdvancedSettingsEnabled ? defaultRoutebutton : null}
-
- - -
- - - - - -
+
{defaultRouteButton}
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4e43749c1753d..f128b8cebc9b0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1676,11 +1676,6 @@ "home.dataManagementDisclaimerPrivacyLink": "プライバシーポリシーをご覧ください。", "home.dataManagementEnableCollection": " 収集を開始するには、 ", "home.dataManagementEnableCollectionLink": "ここで使用状況データを有効にします。", - "home.directory.directoryTitle": "ディレクトリ", - "home.directory.tabs.administrativeTitle": "管理", - "home.directory.tabs.allTitle": "すべて", - "home.directory.tabs.dataTitle": "データの閲覧と可視化", - "home.directory.tabs.otherTitle": "その他", "home.exploreButtonLabel": "独りで閲覧", "home.exploreYourDataDescription": "すべてのステップを終えたら、データ閲覧準備の完了です。", "home.header.title": "ホーム", @@ -2784,7 +2779,6 @@ "kibana-react.kbnOverviewPageHeader.devToolsButtonLabel": "開発ツール", "kibana-react.kbnOverviewPageHeader.stackManagementButtonLabel": "管理", "kibana-react.mountPointPortal.errorMessage": "ポータルコンテンツのレンダリングエラー", - "kibana-react.pageFooter.appDirectoryButtonLabel": "アプリディレクトリを表示", "kibana-react.pageFooter.changeDefaultRouteSuccessToast": "ランディングページが更新されました", "kibana-react.pageFooter.changeHomeRouteLink": "ログイン時に別のページを表示", "kibana-react.pageFooter.makeDefaultRouteLink": "これをランディングページにする", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0682acd0152e5..148287fe4810d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1677,11 +1677,6 @@ "home.dataManagementDisclaimerPrivacyLink": "隐私声明。", "home.dataManagementEnableCollection": " 要启动收集, ", "home.dataManagementEnableCollectionLink": "请在此处启用使用情况数据。", - "home.directory.directoryTitle": "目录", - "home.directory.tabs.administrativeTitle": "管理", - "home.directory.tabs.allTitle": "全部", - "home.directory.tabs.dataTitle": "数据浏览和可视化", - "home.directory.tabs.otherTitle": "其他", "home.exploreButtonLabel": "自己浏览", "home.exploreYourDataDescription": "完成所有步骤后,您便可以随时浏览自己的数据。", "home.header.title": "主页", @@ -2785,7 +2780,6 @@ "kibana-react.kbnOverviewPageHeader.devToolsButtonLabel": "开发工具", "kibana-react.kbnOverviewPageHeader.stackManagementButtonLabel": "管理", "kibana-react.mountPointPortal.errorMessage": "呈现门户内容时出错", - "kibana-react.pageFooter.appDirectoryButtonLabel": "查看应用目录", "kibana-react.pageFooter.changeDefaultRouteSuccessToast": "登陆页面已更新", "kibana-react.pageFooter.changeHomeRouteLink": "登录时显示不同页面", "kibana-react.pageFooter.makeDefaultRouteLink": "将此设为我的登陆页面", From d909a9617fa26d967c71f7574f74b2689b786bbb Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 14 Dec 2020 10:32:57 -0800 Subject: [PATCH 10/44] Revert "[APM] Alerting: Show preview as chart of threshold (#84080)" This reverts commit 9986aff82ec253b9551eddda80cccda34f9e3831. --- .../action_menu/alerting_popover_flyout.tsx | 2 +- .../index.tsx | 0 .../index.stories.tsx | 0 .../index.tsx | 45 ++----- .../PopoverExpression}/index.tsx | 0 .../index.tsx | 5 +- .../index.stories.tsx | 0 .../index.tsx | 78 ++--------- .../index.tsx | 4 +- .../select_anomaly_severity.test.tsx | 0 .../select_anomaly_severity.tsx | 0 .../index.tsx | 59 ++------- .../alerting/chart_preview/index.tsx | 112 ---------------- .../apm/public/components/alerting/fields.tsx | 2 +- .../apm/public/components/alerting/helper.ts | 17 --- .../alerting/register_apm_alerts.ts | 8 +- .../service_alert_trigger.test.tsx | 33 ----- .../chart_preview/get_transaction_duration.ts | 93 ------------- .../get_transaction_error_count.ts | 63 --------- .../get_transaction_error_rate.ts | 84 ------------ .../apm/server/routes/alerts/chart_preview.ts | 72 ---------- .../apm/server/routes/create_apm_api.ts | 12 +- .../basic/tests/alerts/chart_preview.ts | 124 ------------------ .../apm_api_integration/basic/tests/index.ts | 4 - 24 files changed, 43 insertions(+), 774 deletions(-) rename x-pack/plugins/apm/public/components/alerting/{alerting_flyout => AlertingFlyout}/index.tsx (100%) rename x-pack/plugins/apm/public/components/alerting/{error_count_alert_trigger => ErrorCountAlertTrigger}/index.stories.tsx (100%) rename x-pack/plugins/apm/public/components/alerting/{error_count_alert_trigger => ErrorCountAlertTrigger}/index.tsx (65%) rename x-pack/plugins/apm/public/components/alerting/{service_alert_trigger/popover_expression => ServiceAlertTrigger/PopoverExpression}/index.tsx (100%) rename x-pack/plugins/apm/public/components/alerting/{service_alert_trigger => ServiceAlertTrigger}/index.tsx (92%) rename x-pack/plugins/apm/public/components/alerting/{transaction_duration_alert_trigger => TransactionDurationAlertTrigger}/index.stories.tsx (100%) rename x-pack/plugins/apm/public/components/alerting/{transaction_duration_alert_trigger => TransactionDurationAlertTrigger}/index.tsx (70%) rename x-pack/plugins/apm/public/components/alerting/{transaction_duration_anomaly_alert_trigger => TransactionDurationAnomalyAlertTrigger}/index.tsx (96%) rename x-pack/plugins/apm/public/components/alerting/{transaction_duration_anomaly_alert_trigger => TransactionDurationAnomalyAlertTrigger}/select_anomaly_severity.test.tsx (100%) rename x-pack/plugins/apm/public/components/alerting/{transaction_duration_anomaly_alert_trigger => TransactionDurationAnomalyAlertTrigger}/select_anomaly_severity.tsx (100%) rename x-pack/plugins/apm/public/components/alerting/{transaction_error_rate_alert_trigger => TransactionErrorRateAlertTrigger}/index.tsx (71%) delete mode 100644 x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx delete mode 100644 x-pack/plugins/apm/public/components/alerting/helper.ts delete mode 100644 x-pack/plugins/apm/public/components/alerting/service_alert_trigger/service_alert_trigger.test.tsx delete mode 100644 x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts delete mode 100644 x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts delete mode 100644 x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts delete mode 100644 x-pack/plugins/apm/server/routes/alerts/chart_preview.ts delete mode 100644 x-pack/test/apm_api_integration/basic/tests/alerts/chart_preview.ts diff --git a/x-pack/plugins/apm/public/application/action_menu/alerting_popover_flyout.tsx b/x-pack/plugins/apm/public/application/action_menu/alerting_popover_flyout.tsx index 395233735a9d5..394b4caea3e7b 100644 --- a/x-pack/plugins/apm/public/application/action_menu/alerting_popover_flyout.tsx +++ b/x-pack/plugins/apm/public/application/action_menu/alerting_popover_flyout.tsx @@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { IBasePath } from '../../../../../../src/core/public'; import { AlertType } from '../../../common/alert_types'; -import { AlertingFlyout } from '../../components/alerting/alerting_flyout'; +import { AlertingFlyout } from '../../components/alerting/AlertingFlyout'; const alertLabel = i18n.translate('xpack.apm.home.alertsMenu.alerts', { defaultMessage: 'Alerts', diff --git a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx b/x-pack/plugins/apm/public/components/alerting/AlertingFlyout/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx rename to x-pack/plugins/apm/public/components/alerting/AlertingFlyout/index.tsx diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.stories.tsx rename to x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx similarity index 65% rename from x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx rename to x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx index cce973f8587da..efa792ff44273 100644 --- a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx @@ -8,17 +8,12 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { useParams } from 'react-router-dom'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; -import { AlertType, ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; +import { ALERT_TYPES_CONFIG, AlertType } from '../../../../common/alert_types'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; -import { asInteger } from '../../../../common/utils/formatters'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; -import { useFetcher } from '../../../hooks/use_fetcher'; -import { callApmApi } from '../../../services/rest/createCallApmApi'; -import { ChartPreview } from '../chart_preview'; -import { EnvironmentField, IsAboveField, ServiceField } from '../fields'; -import { getAbsoluteTimeRange } from '../helper'; -import { ServiceAlertTrigger } from '../service_alert_trigger'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { EnvironmentField, ServiceField, IsAboveField } from '../fields'; +import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; export interface AlertParams { windowSize: number; @@ -45,23 +40,6 @@ export function ErrorCountAlertTrigger(props: Props) { end, }); - const { threshold, windowSize, windowUnit, environment } = alertParams; - - const { data } = useFetcher(() => { - if (windowSize && windowUnit) { - return callApmApi({ - endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_count', - params: { - query: { - ...getAbsoluteTimeRange(windowSize, windowUnit), - environment, - serviceName, - }, - }, - }); - } - }, [windowSize, windowUnit, environment, serviceName]); - const defaults = { threshold: 25, windowSize: 1, @@ -86,14 +64,14 @@ export function ErrorCountAlertTrigger(props: Props) { unit={i18n.translate('xpack.apm.errorCountAlertTrigger.errors', { defaultMessage: ' errors', })} - onChange={(value) => setAlertParams('threshold', value || 0)} + onChange={(value) => setAlertParams('threshold', value)} />, - setAlertParams('windowSize', timeWindowSize || '') + onChangeWindowSize={(windowSize) => + setAlertParams('windowSize', windowSize || '') } - onChangeWindowUnit={(timeWindowUnit) => - setAlertParams('windowUnit', timeWindowUnit) + onChangeWindowUnit={(windowUnit) => + setAlertParams('windowUnit', windowUnit) } timeWindowSize={params.windowSize} timeWindowUnit={params.windowUnit} @@ -104,10 +82,6 @@ export function ErrorCountAlertTrigger(props: Props) { />, ]; - const chartPreview = ( - - ); - return ( ); } diff --git a/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/popover_expression/index.tsx b/x-pack/plugins/apm/public/components/alerting/ServiceAlertTrigger/PopoverExpression/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/alerting/service_alert_trigger/popover_expression/index.tsx rename to x-pack/plugins/apm/public/components/alerting/ServiceAlertTrigger/PopoverExpression/index.tsx diff --git a/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/ServiceAlertTrigger/index.tsx similarity index 92% rename from x-pack/plugins/apm/public/components/alerting/service_alert_trigger/index.tsx rename to x-pack/plugins/apm/public/components/alerting/ServiceAlertTrigger/index.tsx index 0a12f79bf61a9..b4d3e8f3ad241 100644 --- a/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/ServiceAlertTrigger/index.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGrid, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React, { useEffect } from 'react'; +import { EuiSpacer, EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; import { useParams } from 'react-router-dom'; interface Props { @@ -14,7 +14,6 @@ interface Props { setAlertProperty: (key: string, value: any) => void; defaults: Record; fields: React.ReactNode[]; - chartPreview?: React.ReactNode; } export function ServiceAlertTrigger(props: Props) { @@ -26,7 +25,6 @@ export function ServiceAlertTrigger(props: Props) { setAlertProperty, alertTypeName, defaults, - chartPreview, } = props; const params: Record = { @@ -63,7 +61,6 @@ export function ServiceAlertTrigger(props: Props) { ))} - {chartPreview} ); diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.stories.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.stories.tsx rename to x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.stories.tsx diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx similarity index 70% rename from x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx rename to x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx index f18e407cc58dd..3566850aa24c4 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAlertTrigger/index.tsx @@ -4,31 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ import { EuiSelect } from '@elastic/eui'; +import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { map } from 'lodash'; import React from 'react'; -import { useParams } from 'react-router-dom'; -import { useFetcher } from '../../../../../observability/public'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; -import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; -import { TimeSeries } from '../../../../typings/timeseries'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; -import { callApmApi } from '../../../services/rest/createCallApmApi'; -import { getResponseTimeTickFormatter } from '../../shared/charts/transaction_charts/helper'; -import { useFormatter } from '../../shared/charts/transaction_charts/use_formatter'; -import { ChartPreview } from '../chart_preview'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; +import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { EnvironmentField, - IsAboveField, ServiceField, TransactionTypeField, + IsAboveField, } from '../fields'; -import { getAbsoluteTimeRange } from '../helper'; -import { ServiceAlertTrigger } from '../service_alert_trigger'; -import { PopoverExpression } from '../service_alert_trigger/popover_expression'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; interface AlertParams { windowSize: number; @@ -70,58 +63,14 @@ interface Props { export function TransactionDurationAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; const { urlParams } = useUrlParams(); - const { transactionTypes, transactionType } = useApmServiceContext(); + const { transactionTypes } = useApmServiceContext(); const { serviceName } = useParams<{ serviceName?: string }>(); - const { start, end } = urlParams; + const { start, end, transactionType } = urlParams; const { environmentOptions } = useEnvironmentsFetcher({ serviceName, start, end, }); - const { - aggregationType, - environment, - threshold, - windowSize, - windowUnit, - } = alertParams; - - const { data } = useFetcher(() => { - if (windowSize && windowUnit) { - return callApmApi({ - endpoint: 'GET /api/apm/alerts/chart_preview/transaction_duration', - params: { - query: { - ...getAbsoluteTimeRange(windowSize, windowUnit), - aggregationType, - environment, - serviceName, - transactionType: alertParams.transactionType, - }, - }, - }); - } - }, [ - aggregationType, - environment, - serviceName, - alertParams.transactionType, - windowSize, - windowUnit, - ]); - - const { formatter } = useFormatter([{ data: data ?? [] } as TimeSeries]); - const yTickFormat = getResponseTimeTickFormatter(formatter); - // The threshold from the form is in ms. Convert to µs. - const thresholdMs = threshold * 1000; - - const chartPreview = ( - - ); if (!transactionTypes.length || !serviceName) { return null; @@ -132,7 +81,9 @@ export function TransactionDurationAlertTrigger(props: Props) { aggregationType: 'avg', windowSize: 5, windowUnit: 'm', - transactionType, + + // use the current transaction type or default to the first in the list + transactionType: transactionType || transactionTypes[0], environment: urlParams.environment || ENVIRONMENT_ALL.value, }; @@ -176,7 +127,7 @@ export function TransactionDurationAlertTrigger(props: Props) { unit={i18n.translate('xpack.apm.transactionDurationAlertTrigger.ms', { defaultMessage: 'ms', })} - onChange={(value) => setAlertParams('threshold', value || 0)} + onChange={(value) => setAlertParams('threshold', value)} />, @@ -197,9 +148,8 @@ export function TransactionDurationAlertTrigger(props: Props) { return ( diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx similarity index 96% rename from x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx rename to x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx index 10c4bbff08396..ff5939c601375 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx @@ -11,8 +11,8 @@ import { ANOMALY_SEVERITY } from '../../../../../ml/common'; import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { ServiceAlertTrigger } from '../service_alert_trigger'; -import { PopoverExpression } from '../service_alert_trigger/popover_expression'; +import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; +import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; import { AnomalySeverity, SelectAnomalySeverity, diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.test.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.test.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.test.tsx rename to x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.test.tsx diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.tsx rename to x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.tsx diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx similarity index 71% rename from x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx rename to x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx index 9707df9e86335..f723febde389d 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx @@ -3,26 +3,22 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { useParams } from 'react-router-dom'; +import React from 'react'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; -import { AlertType, ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; -import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; -import { asPercent } from '../../../../common/utils/formatters'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { ALERT_TYPES_CONFIG, AlertType } from '../../../../common/alert_types'; import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; -import { useFetcher } from '../../../hooks/use_fetcher'; -import { callApmApi } from '../../../services/rest/createCallApmApi'; -import { ChartPreview } from '../chart_preview'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; + +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { - EnvironmentField, - IsAboveField, ServiceField, TransactionTypeField, + EnvironmentField, + IsAboveField, } from '../fields'; -import { getAbsoluteTimeRange } from '../helper'; -import { ServiceAlertTrigger } from '../service_alert_trigger'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; interface AlertParams { windowSize: number; @@ -51,32 +47,6 @@ export function TransactionErrorRateAlertTrigger(props: Props) { end, }); - const { threshold, windowSize, windowUnit, environment } = alertParams; - - const thresholdAsPercent = (threshold ?? 0) / 100; - - const { data } = useFetcher(() => { - if (windowSize && windowUnit) { - return callApmApi({ - endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_rate', - params: { - query: { - ...getAbsoluteTimeRange(windowSize, windowUnit), - environment, - serviceName, - transactionType: alertParams.transactionType, - }, - }, - }); - } - }, [ - alertParams.transactionType, - environment, - serviceName, - windowSize, - windowUnit, - ]); - if (serviceName && !transactionTypes.length) { return null; } @@ -109,7 +79,7 @@ export function TransactionErrorRateAlertTrigger(props: Props) { setAlertParams('threshold', value || 0)} + onChange={(value) => setAlertParams('threshold', value)} />, @@ -127,14 +97,6 @@ export function TransactionErrorRateAlertTrigger(props: Props) { />, ]; - const chartPreview = ( - asPercent(d, 1)} - threshold={thresholdAsPercent} - /> - ); - return ( ); } diff --git a/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx b/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx deleted file mode 100644 index 1ed5748cd757e..0000000000000 --- a/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - AnnotationDomainTypes, - Axis, - BarSeries, - Chart, - LineAnnotation, - niceTimeFormatter, - Position, - RectAnnotation, - RectAnnotationDatum, - ScaleType, - Settings, - TickFormatter, -} from '@elastic/charts'; -import { EuiSpacer } from '@elastic/eui'; -import React from 'react'; -import { Coordinate } from '../../../../typings/timeseries'; -import { useTheme } from '../../../hooks/use_theme'; - -interface ChartPreviewProps { - yTickFormat?: TickFormatter; - data?: Coordinate[]; - threshold: number; -} - -export function ChartPreview({ - data = [], - yTickFormat, - threshold, -}: ChartPreviewProps) { - const theme = useTheme(); - const thresholdOpacity = 0.3; - const timestamps = data.map((d) => d.x); - const xMin = Math.min(...timestamps); - const xMax = Math.max(...timestamps); - const xFormatter = niceTimeFormatter([xMin, xMax]); - - // Make the maximum Y value either the actual max or 20% more than the threshold - const values = data.map((d) => d.y ?? 0); - const yMax = Math.max(...values, threshold * 1.2); - - const style = { - fill: theme.eui.euiColorVis9, - line: { - strokeWidth: 2, - stroke: theme.eui.euiColorVis9, - opacity: 1, - }, - opacity: thresholdOpacity, - }; - - const rectDataValues: RectAnnotationDatum[] = [ - { - coordinates: { - x0: null, - x1: null, - y0: threshold, - y1: null, - }, - }, - ]; - - return ( - <> - - - - - - - - - - - ); -} diff --git a/x-pack/plugins/apm/public/components/alerting/fields.tsx b/x-pack/plugins/apm/public/components/alerting/fields.tsx index 9e814bb1b58c5..858604d2baa2a 100644 --- a/x-pack/plugins/apm/public/components/alerting/fields.tsx +++ b/x-pack/plugins/apm/public/components/alerting/fields.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSelectOption } from '@elastic/eui'; import { getEnvironmentLabel } from '../../../common/environment_filter_values'; -import { PopoverExpression } from './service_alert_trigger/popover_expression'; +import { PopoverExpression } from './ServiceAlertTrigger/PopoverExpression'; const ALL_OPTION = i18n.translate('xpack.apm.alerting.fields.all_option', { defaultMessage: 'All', diff --git a/x-pack/plugins/apm/public/components/alerting/helper.ts b/x-pack/plugins/apm/public/components/alerting/helper.ts deleted file mode 100644 index fd3aebc7495a1..0000000000000 --- a/x-pack/plugins/apm/public/components/alerting/helper.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import datemath from '@elastic/datemath'; - -export function getAbsoluteTimeRange(windowSize: number, windowUnit: string) { - const now = new Date().toISOString(); - - return { - start: - datemath.parse(`now-${windowSize}${windowUnit}`)?.toISOString() ?? now, - end: now, - }; -} diff --git a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts index 6dc2cb3163b1f..988e335af5b7c 100644 --- a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts +++ b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts @@ -25,7 +25,7 @@ export function registerApmAlerts( documentationUrl(docLinks) { return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/apm-alerts.html`; }, - alertParamsExpression: lazy(() => import('./error_count_alert_trigger')), + alertParamsExpression: lazy(() => import('./ErrorCountAlertTrigger')), validate: () => ({ errors: [], }), @@ -60,7 +60,7 @@ export function registerApmAlerts( return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/apm-alerts.html`; }, alertParamsExpression: lazy( - () => import('./transaction_duration_alert_trigger') + () => import('./TransactionDurationAlertTrigger') ), validate: () => ({ errors: [], @@ -97,7 +97,7 @@ export function registerApmAlerts( return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/apm-alerts.html`; }, alertParamsExpression: lazy( - () => import('./transaction_error_rate_alert_trigger') + () => import('./TransactionErrorRateAlertTrigger') ), validate: () => ({ errors: [], @@ -134,7 +134,7 @@ export function registerApmAlerts( return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/apm-alerts.html`; }, alertParamsExpression: lazy( - () => import('./transaction_duration_anomaly_alert_trigger') + () => import('./TransactionDurationAnomalyAlertTrigger') ), validate: () => ({ errors: [], diff --git a/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/service_alert_trigger.test.tsx b/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/service_alert_trigger.test.tsx deleted file mode 100644 index 72611043bbed3..0000000000000 --- a/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/service_alert_trigger.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { render } from '@testing-library/react'; -import React, { ReactNode } from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { ServiceAlertTrigger } from './'; - -function Wrapper({ children }: { children?: ReactNode }) { - return {children}; -} - -describe('ServiceAlertTrigger', () => { - it('renders', () => { - expect(() => - render( - {}} - setAlertProperty={() => {}} - />, - { - wrapper: Wrapper, - } - ) - ).not.toThrowError(); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts deleted file mode 100644 index 37e3a2f201fb9..0000000000000 --- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts +++ /dev/null @@ -1,93 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { MetricsAggregationResponsePart } from '../../../../../../typings/elasticsearch/aggregations'; -import { - PROCESSOR_EVENT, - SERVICE_NAME, - TRANSACTION_DURATION, - TRANSACTION_TYPE, -} from '../../../../common/elasticsearch_fieldnames'; -import { ProcessorEvent } from '../../../../common/processor_event'; -import { rangeFilter } from '../../../../common/utils/range_filter'; -import { AlertParams } from '../../../routes/alerts/chart_preview'; -import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; -import { getBucketSize } from '../../helpers/get_bucket_size'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; - -export async function getTransactionDurationChartPreview({ - alertParams, - setup, -}: { - alertParams: AlertParams; - setup: Setup & SetupTimeRange; -}) { - const { apmEventClient, start, end } = setup; - const { - aggregationType, - environment, - serviceName, - transactionType, - } = alertParams; - - const query = { - bool: { - filter: [ - { range: rangeFilter(start, end) }, - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []), - ...(transactionType - ? [{ term: { [TRANSACTION_TYPE]: transactionType } }] - : []), - ...getEnvironmentUiFilterES(environment), - ], - }, - }; - - const { intervalString } = getBucketSize({ start, end, numBuckets: 20 }); - - const aggs = { - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: intervalString, - }, - aggs: { - agg: - aggregationType === 'avg' - ? { avg: { field: TRANSACTION_DURATION } } - : { - percentiles: { - field: TRANSACTION_DURATION, - percents: [aggregationType === '95th' ? 95 : 99], - }, - }, - }, - }, - }; - const params = { - apm: { events: [ProcessorEvent.transaction] }, - body: { size: 0, query, aggs }, - }; - const resp = await apmEventClient.search(params); - - if (!resp.aggregations) { - return []; - } - - return resp.aggregations.timeseries.buckets.map((bucket) => { - const percentilesKey = aggregationType === '95th' ? '95.0' : '99.0'; - const x = bucket.key; - const y = - aggregationType === 'avg' - ? (bucket.agg as MetricsAggregationResponsePart).value - : (bucket.agg as { values: Record }).values[ - percentilesKey - ]; - - return { x, y }; - }); -} diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts deleted file mode 100644 index 28316298aeaad..0000000000000 --- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts +++ /dev/null @@ -1,63 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; -import { ProcessorEvent } from '../../../../common/processor_event'; -import { rangeFilter } from '../../../../common/utils/range_filter'; -import { AlertParams } from '../../../routes/alerts/chart_preview'; -import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; -import { getBucketSize } from '../../helpers/get_bucket_size'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; - -export async function getTransactionErrorCountChartPreview({ - setup, - alertParams, -}: { - setup: Setup & SetupTimeRange; - alertParams: AlertParams; -}) { - const { apmEventClient, start, end } = setup; - const { serviceName, environment } = alertParams; - - const query = { - bool: { - filter: [ - { range: rangeFilter(start, end) }, - ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []), - ...getEnvironmentUiFilterES(environment), - ], - }, - }; - - const { intervalString } = getBucketSize({ start, end, numBuckets: 20 }); - - const aggs = { - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: intervalString, - }, - }, - }; - - const params = { - apm: { events: [ProcessorEvent.error] }, - body: { size: 0, query, aggs }, - }; - - const resp = await apmEventClient.search(params); - - if (!resp.aggregations) { - return []; - } - - return resp.aggregations.timeseries.buckets.map((bucket) => { - return { - x: bucket.key, - y: bucket.doc_count, - }; - }); -} diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts deleted file mode 100644 index fae43ef148cfa..0000000000000 --- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts +++ /dev/null @@ -1,84 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - PROCESSOR_EVENT, - SERVICE_NAME, - TRANSACTION_TYPE, -} from '../../../../common/elasticsearch_fieldnames'; -import { ProcessorEvent } from '../../../../common/processor_event'; -import { rangeFilter } from '../../../../common/utils/range_filter'; -import { AlertParams } from '../../../routes/alerts/chart_preview'; -import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; -import { getBucketSize } from '../../helpers/get_bucket_size'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { - calculateTransactionErrorPercentage, - getOutcomeAggregation, -} from '../../helpers/transaction_error_rate'; - -export async function getTransactionErrorRateChartPreview({ - setup, - alertParams, -}: { - setup: Setup & SetupTimeRange; - alertParams: AlertParams; -}) { - const { apmEventClient, start, end } = setup; - const { serviceName, environment, transactionType } = alertParams; - - const query = { - bool: { - filter: [ - { range: rangeFilter(start, end) }, - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []), - ...(transactionType - ? [{ term: { [TRANSACTION_TYPE]: transactionType } }] - : []), - ...getEnvironmentUiFilterES(environment), - ], - }, - }; - - const outcomes = getOutcomeAggregation({ - searchAggregatedTransactions: false, - }); - - const { intervalString } = getBucketSize({ start, end, numBuckets: 20 }); - - const aggs = { - outcomes, - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: intervalString, - }, - aggs: { outcomes }, - }, - }; - - const params = { - apm: { events: [ProcessorEvent.transaction] }, - body: { size: 0, query, aggs }, - }; - - const resp = await apmEventClient.search(params); - - if (!resp.aggregations) { - return []; - } - - return resp.aggregations.timeseries.buckets.map((bucket) => { - const errorPercentage = calculateTransactionErrorPercentage( - bucket.outcomes - ); - return { - x: bucket.key, - y: errorPercentage, - }; - }); -} diff --git a/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts b/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts deleted file mode 100644 index dc8bf45de091b..0000000000000 --- a/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import * as t from 'io-ts'; -import { getTransactionDurationChartPreview } from '../../lib/alerts/chart_preview/get_transaction_duration'; -import { getTransactionErrorCountChartPreview } from '../../lib/alerts/chart_preview/get_transaction_error_count'; -import { getTransactionErrorRateChartPreview } from '../../lib/alerts/chart_preview/get_transaction_error_rate'; -import { setupRequest } from '../../lib/helpers/setup_request'; -import { createRoute } from '../create_route'; -import { rangeRt } from '../default_api_types'; - -const alertParamsRt = t.intersection([ - t.partial({ - aggregationType: t.union([ - t.literal('avg'), - t.literal('95th'), - t.literal('99th'), - ]), - serviceName: t.string, - environment: t.string, - transactionType: t.string, - }), - rangeRt, -]); - -export type AlertParams = t.TypeOf; - -export const transactionErrorRateChartPreview = createRoute({ - endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_rate', - params: t.type({ query: alertParamsRt }), - options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { _debug, ...alertParams } = context.params.query; - - return getTransactionErrorRateChartPreview({ - setup, - alertParams, - }); - }, -}); - -export const transactionErrorCountChartPreview = createRoute({ - endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_count', - params: t.type({ query: alertParamsRt }), - options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { _debug, ...alertParams } = context.params.query; - return getTransactionErrorCountChartPreview({ - setup, - alertParams, - }); - }, -}); - -export const transactionDurationChartPreview = createRoute({ - endpoint: 'GET /api/apm/alerts/chart_preview/transaction_duration', - params: t.type({ query: alertParamsRt }), - options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { _debug, ...alertParams } = context.params.query; - - return getTransactionDurationChartPreview({ - alertParams, - setup, - }); - }, -}); 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 b09175a6841f8..d34e67083b037 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -101,11 +101,6 @@ import { rumVisitorsBreakdownRoute, rumWebCoreVitals, } from './rum_client'; -import { - transactionErrorRateChartPreview, - transactionErrorCountChartPreview, - transactionDurationChartPreview, -} from './alerts/chart_preview'; const createApmApi = () => { const api = createApi() @@ -209,12 +204,7 @@ const createApmApi = () => { .add(rumJSErrors) .add(rumUrlSearch) .add(rumLongTaskMetrics) - .add(rumHasDataRoute) - - // Alerting - .add(transactionErrorCountChartPreview) - .add(transactionDurationChartPreview) - .add(transactionErrorRateChartPreview); + .add(rumHasDataRoute); return api; }; diff --git a/x-pack/test/apm_api_integration/basic/tests/alerts/chart_preview.ts b/x-pack/test/apm_api_integration/basic/tests/alerts/chart_preview.ts deleted file mode 100644 index 3119de47a8635..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/alerts/chart_preview.ts +++ /dev/null @@ -1,124 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { format } from 'url'; -import archives from '../../../common/archives_metadata'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); - const supertest = getService('supertest'); - const archiveName = 'apm_8.0.0'; - const { end } = archives[archiveName]; - const start = new Date(Date.parse(end) - 600000).toISOString(); - - describe('Alerting chart previews', () => { - describe('GET /api/apm/alerts/chart_preview/transaction_error_rate', () => { - const url = format({ - pathname: '/api/apm/alerts/chart_preview/transaction_error_rate', - query: { - start, - end, - transactionType: 'request', - serviceName: 'opbeans-java', - }, - }); - - describe('when data is not loaded', () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect(response.body).to.eql([]); - }); - }); - - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - - it('returns the correct data', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect( - response.body.some((item: { x: number; y: number | null }) => item.x && item.y) - ).to.equal(true); - }); - }); - }); - - describe('GET /api/apm/alerts/chart_preview/transaction_error_count', () => { - const url = format({ - pathname: '/api/apm/alerts/chart_preview/transaction_error_count', - query: { - start, - end, - serviceName: 'opbeans-java', - }, - }); - - describe('when data is not loaded', () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect(response.body).to.eql([]); - }); - }); - - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - - it('returns the correct data', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect( - response.body.some((item: { x: number; y: number | null }) => item.x && item.y) - ).to.equal(true); - }); - }); - }); - - describe('GET /api/apm/alerts/chart_preview/transaction_duration', () => { - const url = format({ - pathname: '/api/apm/alerts/chart_preview/transaction_duration', - query: { - start, - end, - serviceName: 'opbeans-java', - transactionType: 'request', - }, - }); - - describe('when data is not loaded', () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect(response.body).to.eql([]); - }); - }); - - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - - it('returns the correct data', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect( - response.body.some((item: { x: number; y: number | null }) => item.x && item.y) - ).to.equal(true); - }); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/basic/tests/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts index c0156d92439f0..3e625688e2459 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -11,10 +11,6 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont loadTestFile(require.resolve('./feature_controls')); - describe('Alerts', function () { - loadTestFile(require.resolve('./alerts/chart_preview')); - }); - describe('Service Maps', function () { loadTestFile(require.resolve('./service_maps/service_maps')); }); From 06993c469b7baf23e81d268ecdcb4ee13131fc36 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Mon, 14 Dec 2020 13:45:56 -0500 Subject: [PATCH 11/44] [Fleet] Installation of hidden field (#85703) Co-authored-by: nnamdifrankie Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/fleet/common/types/models/epm.ts | 3 ++- .../epm/elasticsearch/template/install.ts | 1 + .../elasticsearch/template/template.test.ts | 25 +++++++++++++++++++ .../epm/elasticsearch/template/template.ts | 16 +++++++++--- 4 files changed, 41 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 96868fa8cfc3b..f518c606d6959 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -207,6 +207,7 @@ export type ElasticsearchAssetTypeToParts = Record< export interface RegistryDataStream { type: string; + hidden?: boolean; dataset: string; title: string; release: string; @@ -319,7 +320,7 @@ export interface IndexTemplate { mappings: any; aliases: object; }; - data_stream: object; + data_stream: { hidden?: boolean }; composed_of: string[]; _meta: object; } diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index 199026da30c11..944f742e54546 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -314,6 +314,7 @@ export async function installTemplate({ pipelineName, packageName, composedOfTemplates, + hidden: dataStream.hidden, }); // TODO: Check return values for errors diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts index cc1aa79c7491c..bdff7e0fb3bc6 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts @@ -60,6 +60,31 @@ test('adds empty composed_of correctly', () => { expect(template.composed_of).toStrictEqual(composedOfTemplates); }); +test('adds hidden field correctly', () => { + const templateWithHiddenName = 'logs-nginx-access-abcd'; + + const templateWithHidden = getTemplate({ + type: 'logs', + templateName: templateWithHiddenName, + packageName: 'nginx', + mappings: { properties: {} }, + composedOfTemplates: [], + hidden: true, + }); + expect(templateWithHidden.data_stream.hidden).toEqual(true); + + const templateWithoutHiddenName = 'logs-nginx-access-efgh'; + + const templateWithoutHidden = getTemplate({ + type: 'logs', + templateName: templateWithoutHiddenName, + packageName: 'nginx', + mappings: { properties: {} }, + composedOfTemplates: [], + }); + expect(templateWithoutHidden.data_stream.hidden).toEqual(undefined); +}); + test('tests loading base.yml', () => { const ymlPath = path.join(__dirname, '../../fields/tests/base.yml'); const fieldsYML = readFileSync(ymlPath, 'utf-8'); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts index 8d33180d6262d..d80d54d098db7 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts @@ -45,6 +45,7 @@ export function getTemplate({ pipelineName, packageName, composedOfTemplates, + hidden, }: { type: string; templateName: string; @@ -52,8 +53,16 @@ export function getTemplate({ pipelineName?: string | undefined; packageName: string; composedOfTemplates: string[]; + hidden?: boolean; }): IndexTemplate { - const template = getBaseTemplate(type, templateName, mappings, packageName, composedOfTemplates); + const template = getBaseTemplate( + type, + templateName, + mappings, + packageName, + composedOfTemplates, + hidden + ); if (pipelineName) { template.template.settings.index.default_pipeline = pipelineName; } @@ -253,7 +262,8 @@ function getBaseTemplate( templateName: string, mappings: IndexTemplateMappings, packageName: string, - composedOfTemplates: string[] + composedOfTemplates: string[], + hidden?: boolean ): IndexTemplate { // Meta information to identify Ingest Manager's managed templates and indices const _meta = { @@ -324,7 +334,7 @@ function getBaseTemplate( // To be filled with the aliases that we need aliases: {}, }, - data_stream: {}, + data_stream: { hidden }, composed_of: composedOfTemplates, _meta, }; From 5f6ed3dc3ccd034f4e067e4a3d181f1f0c82a5d1 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Mon, 14 Dec 2020 12:51:14 -0600 Subject: [PATCH 12/44] skip custom detection rules. #83772 --- .../cypress/integration/alerts_detection_rules_custom.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index b3c82a8d9d6f0..3ce507c791f0a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -217,7 +217,7 @@ describe('Custom detection rules creation', () => { }); }); -describe('Custom detection rules deletion and edition', () => { +describe.skip('Custom detection rules deletion and edition', () => { beforeEach(() => { esArchiverLoad('custom_rules'); loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); From ea4e2224a93bef2ee31f203a88baa71e5594ec9b Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 14 Dec 2020 21:11:53 +0200 Subject: [PATCH 13/44] [Security Solution][Case] Sync cases with alerts (#84731) --- x-pack/plugins/case/common/api/cases/case.ts | 8 +- .../case/common/api/cases/user_actions.ts | 1 + x-pack/plugins/case/kibana.json | 2 +- .../server/client/alerts/update_status.ts | 25 +++++ .../case/server/client/cases/create.test.ts | 28 +++++- .../case/server/client/cases/create.ts | 2 +- .../case/server/client/cases/update.test.ts | 38 ++++++-- .../case/server/client/cases/update.ts | 65 ++++++++++++- .../case/server/client/comments/add.test.ts | 9 ++ .../case/server/client/comments/add.ts | 24 ++++- .../plugins/case/server/client/index.test.ts | 26 +++++- x-pack/plugins/case/server/client/index.ts | 18 ++++ x-pack/plugins/case/server/client/mocks.ts | 54 +++++++++-- x-pack/plugins/case/server/client/types.ts | 16 +++- .../case/server/connectors/case/index.test.ts | 92 ++++++++++++++++++- .../case/server/connectors/case/index.ts | 29 +++++- .../case/server/connectors/case/schema.ts | 30 ++++-- .../plugins/case/server/connectors/index.ts | 4 + x-pack/plugins/case/server/plugin.ts | 21 ++++- .../api/__fixtures__/mock_saved_objects.ts | 15 +++ .../routes/api/__fixtures__/route_contexts.ts | 39 +++++--- .../routes/api/__mocks__/request_responses.ts | 3 + .../routes/api/cases/comments/post_comment.ts | 2 +- .../routes/api/cases/patch_cases.test.ts | 9 ++ .../server/routes/api/cases/patch_cases.ts | 2 +- .../server/routes/api/cases/post_case.test.ts | 18 ++++ .../case/server/routes/api/utils.test.ts | 12 +++ .../case/server/saved_object_types/cases.ts | 7 ++ .../server/saved_object_types/migrations.ts | 36 ++++++-- .../case/server/services/alerts/index.ts | 57 ++++++++++++ x-pack/plugins/case/server/services/index.ts | 1 + x-pack/plugins/case/server/services/mocks.ts | 13 ++- .../server/services/user_actions/helpers.ts | 1 + x-pack/plugins/case/server/types.ts | 5 + .../cases/components/all_cases/index.test.tsx | 3 + .../components/case_action_bar/index.tsx | 75 +++++++++++---- .../case_settings/sync_alerts_switch.tsx | 48 ++++++++++ .../cases/components/case_view/index.tsx | 21 ++++- .../cases/components/create/connector.tsx | 4 +- .../cases/components/create/form.test.tsx | 1 + .../public/cases/components/create/form.tsx | 20 +++- .../cases/components/create/form_context.tsx | 12 ++- .../cases/components/create/index.test.tsx | 8 +- .../public/cases/components/create/schema.tsx | 11 ++- .../components/create/sync_alerts_toggle.tsx | 37 ++++++++ .../cases/components/create/translations.ts | 14 +++ .../user_action_alert_comment_event.tsx | 7 +- .../public/cases/containers/api.test.tsx | 3 + .../public/cases/containers/mock.ts | 3 + .../public/cases/containers/types.ts | 2 + .../public/cases/containers/use_get_case.tsx | 3 + .../cases/containers/use_post_case.test.tsx | 3 + .../cases/containers/use_update_case.tsx | 2 +- .../public/cases/translations.ts | 22 +++++ .../plugins/security_solution/server/index.ts | 2 + .../basic/tests/cases/migrations.ts | 13 +++ .../user_actions/get_all_user_actions.ts | 11 ++- .../basic/tests/connectors/case.ts | 64 ++++++++++++- .../case_api_integration/common/lib/mock.ts | 3 + 59 files changed, 996 insertions(+), 108 deletions(-) create mode 100644 x-pack/plugins/case/server/client/alerts/update_status.ts create mode 100644 x-pack/plugins/case/server/services/alerts/index.ts create mode 100644 x-pack/plugins/security_solution/public/cases/components/case_settings/sync_alerts_switch.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/create/sync_alerts_toggle.tsx diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts index 9b99bf0e54cc2..a08e1fbca66ea 100644 --- a/x-pack/plugins/case/common/api/cases/case.ts +++ b/x-pack/plugins/case/common/api/cases/case.ts @@ -29,12 +29,17 @@ const CaseStatusRt = rt.union([ export const caseStatuses = Object.values(CaseStatuses); +const SettingsRt = rt.type({ + syncAlerts: rt.boolean, +}); + const CaseBasicRt = rt.type({ - connector: CaseConnectorRt, description: rt.string, status: CaseStatusRt, tags: rt.array(rt.string), title: rt.string, + connector: CaseConnectorRt, + settings: SettingsRt, }); const CaseExternalServiceBasicRt = rt.type({ @@ -74,6 +79,7 @@ export const CasePostRequestRt = rt.type({ tags: rt.array(rt.string), title: rt.string, connector: CaseConnectorRt, + settings: SettingsRt, }); export const CaseExternalServiceRequestRt = CaseExternalServiceBasicRt; diff --git a/x-pack/plugins/case/common/api/cases/user_actions.ts b/x-pack/plugins/case/common/api/cases/user_actions.ts index 1a3ccfc04eed9..e7aa67db9287e 100644 --- a/x-pack/plugins/case/common/api/cases/user_actions.ts +++ b/x-pack/plugins/case/common/api/cases/user_actions.ts @@ -20,6 +20,7 @@ const UserActionFieldRt = rt.array( rt.literal('tags'), rt.literal('title'), rt.literal('status'), + rt.literal('settings'), ]) ); const UserActionRt = rt.union([ diff --git a/x-pack/plugins/case/kibana.json b/x-pack/plugins/case/kibana.json index 55416ee28c7df..2048ae41fa8ab 100644 --- a/x-pack/plugins/case/kibana.json +++ b/x-pack/plugins/case/kibana.json @@ -2,7 +2,7 @@ "configPath": ["xpack", "case"], "id": "case", "kibanaVersion": "kibana", - "requiredPlugins": ["actions"], + "requiredPlugins": ["actions", "securitySolution"], "optionalPlugins": [ "spaces", "security" diff --git a/x-pack/plugins/case/server/client/alerts/update_status.ts b/x-pack/plugins/case/server/client/alerts/update_status.ts new file mode 100644 index 0000000000000..d90424eb5fb15 --- /dev/null +++ b/x-pack/plugins/case/server/client/alerts/update_status.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from '@hapi/boom'; +import { CaseClientUpdateAlertsStatus, CaseClientFactoryArguments } from '../types'; + +export const updateAlertsStatus = ({ + alertsService, + request, + context, +}: CaseClientFactoryArguments) => async ({ + ids, + status, +}: CaseClientUpdateAlertsStatus): Promise => { + const securitySolutionClient = context?.securitySolution?.getAppClient(); + if (securitySolutionClient == null) { + throw Boom.notFound('securitySolutionClient client have not been found'); + } + + const index = securitySolutionClient.getSignalsIndex(); + await alertsService.updateAlertsStatus({ ids, status, index, request }); +}; diff --git a/x-pack/plugins/case/server/client/cases/create.test.ts b/x-pack/plugins/case/server/client/cases/create.test.ts index e09ce226b3125..90116e3728883 100644 --- a/x-pack/plugins/case/server/client/cases/create.test.ts +++ b/x-pack/plugins/case/server/client/cases/create.test.ts @@ -34,6 +34,9 @@ describe('create', () => { type: ConnectorTypes.jira, fields: { issueType: 'Task', priority: 'High', parent: null }, }, + settings: { + syncAlerts: true, + }, } as CasePostRequest; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -65,6 +68,9 @@ describe('create', () => { updated_at: null, updated_by: null, version: 'WzksMV0=', + settings: { + syncAlerts: true, + }, }); expect( @@ -79,9 +85,9 @@ describe('create', () => { full_name: 'Awesome D00d', username: 'awesome', }, - action_field: ['description', 'status', 'tags', 'title', 'connector'], + action_field: ['description', 'status', 'tags', 'title', 'connector', 'settings'], new_value: - '{"description":"This is a brand new case of a bad meanie defacing data","title":"Super Bad Security Issue","tags":["defacement"],"connector":{"id":"123","name":"Jira","type":".jira","fields":{"issueType":"Task","priority":"High","parent":null}}}', + '{"description":"This is a brand new case of a bad meanie defacing data","title":"Super Bad Security Issue","tags":["defacement"],"connector":{"id":"123","name":"Jira","type":".jira","fields":{"issueType":"Task","priority":"High","parent":null}},"settings":{"syncAlerts":true}}', old_value: null, }, references: [ @@ -106,6 +112,9 @@ describe('create', () => { type: ConnectorTypes.none, fields: null, }, + settings: { + syncAlerts: true, + }, }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -131,6 +140,9 @@ describe('create', () => { updated_at: null, updated_by: null, version: 'WzksMV0=', + settings: { + syncAlerts: true, + }, }); }); @@ -145,6 +157,9 @@ describe('create', () => { type: ConnectorTypes.none, fields: null, }, + settings: { + syncAlerts: true, + }, }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -174,6 +189,9 @@ describe('create', () => { updated_at: null, updated_by: null, version: 'WzksMV0=', + settings: { + syncAlerts: true, + }, }); }); }); @@ -323,6 +341,9 @@ describe('create', () => { type: ConnectorTypes.none, fields: null, }, + settings: { + syncAlerts: true, + }, }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -347,6 +368,9 @@ describe('create', () => { type: ConnectorTypes.none, fields: null, }, + settings: { + syncAlerts: true, + }, }; const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, diff --git a/x-pack/plugins/case/server/client/cases/create.ts b/x-pack/plugins/case/server/client/cases/create.ts index 59222be062c75..1dca025036c1e 100644 --- a/x-pack/plugins/case/server/client/cases/create.ts +++ b/x-pack/plugins/case/server/client/cases/create.ts @@ -64,7 +64,7 @@ export const create = ({ actionAt: createdDate, actionBy: { username, full_name, email }, caseId: newCase.id, - fields: ['description', 'status', 'tags', 'title', 'connector'], + fields: ['description', 'status', 'tags', 'title', 'connector', 'settings'], newValue: JSON.stringify(query), }), ], diff --git a/x-pack/plugins/case/server/client/cases/update.test.ts b/x-pack/plugins/case/server/client/cases/update.test.ts index ae701f16b2bcb..1f9e8cc788404 100644 --- a/x-pack/plugins/case/server/client/cases/update.test.ts +++ b/x-pack/plugins/case/server/client/cases/update.test.ts @@ -38,7 +38,10 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - const res = await caseClient.client.update({ cases: patchCases }); + const res = await caseClient.client.update({ + caseClient: caseClient.client, + cases: patchCases, + }); expect(res).toEqual([ { @@ -63,6 +66,9 @@ describe('update', () => { updated_at: '2019-11-25T21:54:48.952Z', updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, version: 'WzE3LDFd', + settings: { + syncAlerts: true, + }, }, ]); @@ -115,7 +121,10 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - const res = await caseClient.client.update({ cases: patchCases }); + const res = await caseClient.client.update({ + caseClient: caseClient.client, + cases: patchCases, + }); expect(res).toEqual([ { @@ -140,6 +149,9 @@ describe('update', () => { updated_at: '2019-11-25T21:54:48.952Z', updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, version: 'WzE3LDFd', + settings: { + syncAlerts: true, + }, }, ]); }); @@ -160,7 +172,10 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - const res = await caseClient.client.update({ cases: patchCases }); + const res = await caseClient.client.update({ + caseClient: caseClient.client, + cases: patchCases, + }); expect(res).toEqual([ { @@ -185,6 +200,9 @@ describe('update', () => { updated_at: '2019-11-25T21:54:48.952Z', updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, version: 'WzE3LDFd', + settings: { + syncAlerts: true, + }, }, ]); }); @@ -210,7 +228,10 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - const res = await caseClient.client.update({ cases: patchCases }); + const res = await caseClient.client.update({ + caseClient: caseClient.client, + cases: patchCases, + }); expect(res).toEqual([ { @@ -243,6 +264,9 @@ describe('update', () => { username: 'awesome', }, version: 'WzE3LDFd', + settings: { + syncAlerts: true, + }, }, ]); }); @@ -328,7 +352,7 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - caseClient.client.update({ cases: patchCases }).catch((e) => { + caseClient.client.update({ caseClient: caseClient.client, cases: patchCases }).catch((e) => { expect(e).not.toBeNull(); expect(e.isBoom).toBe(true); expect(e.output.statusCode).toBe(406); @@ -358,7 +382,7 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - caseClient.client.update({ cases: patchCases }).catch((e) => { + caseClient.client.update({ caseClient: caseClient.client, cases: patchCases }).catch((e) => { expect(e).not.toBeNull(); expect(e.isBoom).toBe(true); expect(e.output.statusCode).toBe(404); @@ -385,7 +409,7 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); - caseClient.client.update({ cases: patchCases }).catch((e) => { + caseClient.client.update({ caseClient: caseClient.client, cases: patchCases }).catch((e) => { expect(e).not.toBeNull(); expect(e.isBoom).toBe(true); expect(e.output.statusCode).toBe(409); diff --git a/x-pack/plugins/case/server/client/cases/update.ts b/x-pack/plugins/case/server/client/cases/update.ts index 406e43a74cccf..e2b6cb8337251 100644 --- a/x-pack/plugins/case/server/client/cases/update.ts +++ b/x-pack/plugins/case/server/client/cases/update.ts @@ -9,6 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; +import { SavedObjectsFindResponse } from 'kibana/server'; import { flattenCaseSavedObject } from '../../routes/api/utils'; import { @@ -34,7 +35,10 @@ export const update = ({ caseService, userActionService, request, -}: CaseClientFactoryArguments) => async ({ cases }: CaseClientUpdate): Promise => { +}: CaseClientFactoryArguments) => async ({ + caseClient, + cases, +}: CaseClientUpdate): Promise => { const query = pipe( excess(CasesPatchRequestRt).decode(cases), fold(throwErrors(Boom.badRequest), identity) @@ -126,6 +130,65 @@ export const update = ({ }), }); + // If a status update occurred and the case is synced then we need to update all alerts' status + // attached to the case to the new status. + const casesWithStatusChangedAndSynced = updateFilterCases.filter((caseToUpdate) => { + const currentCase = myCases.saved_objects.find((c) => c.id === caseToUpdate.id); + return ( + currentCase != null && + caseToUpdate.status != null && + currentCase.attributes.status !== caseToUpdate.status && + currentCase.attributes.settings.syncAlerts + ); + }); + + // If syncAlerts setting turned on we need to update all alerts' status + // attached to the case to the current status. + const casesWithSyncSettingChangedToOn = updateFilterCases.filter((caseToUpdate) => { + const currentCase = myCases.saved_objects.find((c) => c.id === caseToUpdate.id); + return ( + currentCase != null && + caseToUpdate.settings?.syncAlerts != null && + currentCase.attributes.settings.syncAlerts !== caseToUpdate.settings.syncAlerts && + caseToUpdate.settings.syncAlerts + ); + }); + + for (const theCase of [ + ...casesWithSyncSettingChangedToOn, + ...casesWithStatusChangedAndSynced, + ]) { + const currentCase = myCases.saved_objects.find((c) => c.id === theCase.id); + const totalComments = await caseService.getAllCaseComments({ + client: savedObjectsClient, + caseId: theCase.id, + options: { + fields: [], + filter: 'cases-comments.attributes.type: alert', + page: 1, + perPage: 1, + }, + }); + + const caseComments = (await caseService.getAllCaseComments({ + client: savedObjectsClient, + caseId: theCase.id, + options: { + fields: [], + filter: 'cases-comments.attributes.type: alert', + page: 1, + perPage: totalComments.total, + }, + // The filter guarantees that the comments will be of type alert + })) as SavedObjectsFindResponse<{ alertId: string }>; + + caseClient.updateAlertsStatus({ + ids: caseComments.saved_objects.map(({ attributes: { alertId } }) => alertId), + // Either there is a status update or the syncAlerts got turned on. + status: theCase.status ?? currentCase?.attributes.status ?? CaseStatuses.open, + }); + } + const returnUpdatedCase = myCases.saved_objects .filter((myCase) => updatedCases.saved_objects.some((updatedCase) => updatedCase.id === myCase.id) diff --git a/x-pack/plugins/case/server/client/comments/add.test.ts b/x-pack/plugins/case/server/client/comments/add.test.ts index d00df5a3246bd..40b87f6ad17f0 100644 --- a/x-pack/plugins/case/server/client/comments/add.test.ts +++ b/x-pack/plugins/case/server/client/comments/add.test.ts @@ -31,6 +31,7 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); const res = await caseClient.client.addComment({ + caseClient: caseClient.client, caseId: 'mock-id-1', comment: { comment: 'Wow, good luck catching that bad meanie!', @@ -66,6 +67,7 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); const res = await caseClient.client.addComment({ + caseClient: caseClient.client, caseId: 'mock-id-1', comment: { type: CommentType.alert, @@ -103,6 +105,7 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); const res = await caseClient.client.addComment({ + caseClient: caseClient.client, caseId: 'mock-id-1', comment: { comment: 'Wow, good luck catching that bad meanie!', @@ -126,6 +129,7 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); await caseClient.client.addComment({ + caseClient: caseClient.client, caseId: 'mock-id-1', comment: { comment: 'Wow, good luck catching that bad meanie!', @@ -173,6 +177,7 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient, true); const res = await caseClient.client.addComment({ + caseClient: caseClient.client, caseId: 'mock-id-1', comment: { comment: 'Wow, good luck catching that bad meanie!', @@ -267,6 +272,7 @@ describe('addComment', () => { ['alertId', 'index'].forEach((attribute) => { caseClient.client .addComment({ + caseClient: caseClient.client, caseId: 'mock-id-1', comment: { [attribute]: attribute, @@ -328,6 +334,7 @@ describe('addComment', () => { ['comment'].forEach((attribute) => { caseClient.client .addComment({ + caseClient: caseClient.client, caseId: 'mock-id-1', comment: { [attribute]: attribute, @@ -354,6 +361,7 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); caseClient.client .addComment({ + caseClient: caseClient.client, caseId: 'not-exists', comment: { comment: 'Wow, good luck catching that bad meanie!', @@ -377,6 +385,7 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient); caseClient.client .addComment({ + caseClient: caseClient.client, caseId: 'mock-id-1', comment: { comment: 'Throw an error', diff --git a/x-pack/plugins/case/server/client/comments/add.ts b/x-pack/plugins/case/server/client/comments/add.ts index 169157c95d4c1..bb61094cfa3bd 100644 --- a/x-pack/plugins/case/server/client/comments/add.ts +++ b/x-pack/plugins/case/server/client/comments/add.ts @@ -11,7 +11,14 @@ import { identity } from 'fp-ts/lib/function'; import { decodeComment, flattenCaseSavedObject, transformNewComment } from '../../routes/api/utils'; -import { throwErrors, CaseResponseRt, CommentRequestRt, CaseResponse } from '../../../common/api'; +import { + throwErrors, + CaseResponseRt, + CommentRequestRt, + CaseResponse, + CommentType, + CaseStatuses, +} from '../../../common/api'; import { buildCommentUserActionItem } from '../../services/user_actions/helpers'; import { CaseClientAddComment, CaseClientFactoryArguments } from '../types'; @@ -23,11 +30,11 @@ export const addComment = ({ userActionService, request, }: CaseClientFactoryArguments) => async ({ + caseClient, caseId, comment, }: CaseClientAddComment): Promise => { const query = pipe( - // TODO: Excess CommentRequestRt when the excess() function supports union types CommentRequestRt.decode(comment), fold(throwErrors(Boom.badRequest), identity) ); @@ -39,6 +46,11 @@ export const addComment = ({ caseId, }); + // An alert cannot be attach to a closed case. + if (query.type === CommentType.alert && myCase.attributes.status === CaseStatuses.closed) { + throw Boom.badRequest('Alert cannot be attached to a closed case'); + } + // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = await caseService.getUser({ request }); const createdDate = new Date().toISOString(); @@ -72,6 +84,14 @@ export const addComment = ({ }), ]); + // If the case is synced with alerts the newly attached alert must match the status of the case. + if (newComment.attributes.type === CommentType.alert && myCase.attributes.settings.syncAlerts) { + caseClient.updateAlertsStatus({ + ids: [newComment.attributes.alertId], + status: myCase.attributes.status, + }); + } + const totalCommentsFindByCases = await caseService.getAllCaseComments({ client: savedObjectsClient, caseId, diff --git a/x-pack/plugins/case/server/client/index.test.ts b/x-pack/plugins/case/server/client/index.test.ts index 1ecdc8ea96dea..ef4491204d9f5 100644 --- a/x-pack/plugins/case/server/client/index.test.ts +++ b/x-pack/plugins/case/server/client/index.test.ts @@ -4,32 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest } from 'kibana/server'; +import { KibanaRequest, RequestHandlerContext } from 'kibana/server'; import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { createCaseClient } from '.'; import { createCaseServiceMock, createConfigureServiceMock, createUserActionServiceMock, + createAlertServiceMock, } from '../services/mocks'; import { create } from './cases/create'; import { update } from './cases/update'; import { addComment } from './comments/add'; +import { updateAlertsStatus } from './alerts/update_status'; jest.mock('./cases/create'); jest.mock('./cases/update'); jest.mock('./comments/add'); +jest.mock('./alerts/update_status'); const caseService = createCaseServiceMock(); const caseConfigureService = createConfigureServiceMock(); const userActionService = createUserActionServiceMock(); +const alertsService = createAlertServiceMock(); const savedObjectsClient = savedObjectsClientMock.create(); const request = {} as KibanaRequest; +const context = {} as RequestHandlerContext; const createMock = create as jest.Mock; const updateMock = update as jest.Mock; const addCommentMock = addComment as jest.Mock; +const updateAlertsStatusMock = updateAlertsStatus as jest.Mock; describe('createCaseClient()', () => { test('it creates the client correctly', async () => { @@ -39,6 +45,8 @@ describe('createCaseClient()', () => { caseConfigureService, caseService, userActionService, + alertsService, + context, }); expect(createMock).toHaveBeenCalledWith({ @@ -47,6 +55,8 @@ describe('createCaseClient()', () => { caseConfigureService, caseService, userActionService, + alertsService, + context, }); expect(updateMock).toHaveBeenCalledWith({ @@ -55,6 +65,8 @@ describe('createCaseClient()', () => { caseConfigureService, caseService, userActionService, + alertsService, + context, }); expect(addCommentMock).toHaveBeenCalledWith({ @@ -63,6 +75,18 @@ describe('createCaseClient()', () => { caseConfigureService, caseService, userActionService, + alertsService, + context, + }); + + expect(updateAlertsStatusMock).toHaveBeenCalledWith({ + savedObjectsClient, + request, + caseConfigureService, + caseService, + userActionService, + alertsService, + context, }); }); }); diff --git a/x-pack/plugins/case/server/client/index.ts b/x-pack/plugins/case/server/client/index.ts index 75e9e3c4cfebc..bf43921b46466 100644 --- a/x-pack/plugins/case/server/client/index.ts +++ b/x-pack/plugins/case/server/client/index.ts @@ -8,6 +8,7 @@ import { CaseClientFactoryArguments, CaseClient } from './types'; import { create } from './cases/create'; import { update } from './cases/update'; import { addComment } from './comments/add'; +import { updateAlertsStatus } from './alerts/update_status'; export { CaseClient } from './types'; @@ -17,6 +18,8 @@ export const createCaseClient = ({ caseConfigureService, caseService, userActionService, + alertsService, + context, }: CaseClientFactoryArguments): CaseClient => { return { create: create({ @@ -25,6 +28,8 @@ export const createCaseClient = ({ caseConfigureService, caseService, userActionService, + alertsService, + context, }), update: update({ savedObjectsClient, @@ -32,6 +37,8 @@ export const createCaseClient = ({ caseConfigureService, caseService, userActionService, + alertsService, + context, }), addComment: addComment({ savedObjectsClient, @@ -39,6 +46,17 @@ export const createCaseClient = ({ caseConfigureService, caseService, userActionService, + alertsService, + context, + }), + updateAlertsStatus: updateAlertsStatus({ + savedObjectsClient, + request, + caseConfigureService, + caseService, + userActionService, + alertsService, + context, }), }; }; diff --git a/x-pack/plugins/case/server/client/mocks.ts b/x-pack/plugins/case/server/client/mocks.ts index 243dd884f9ef6..dd4e8b52b4dc6 100644 --- a/x-pack/plugins/case/server/client/mocks.ts +++ b/x-pack/plugins/case/server/client/mocks.ts @@ -4,18 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest } from 'kibana/server'; -import { loggingSystemMock } from '../../../../../src/core/server/mocks'; -import { CaseService, CaseConfigureService, CaseUserActionServiceSetup } from '../services'; +import { KibanaRequest, RequestHandlerContext } from 'kibana/server'; +import { loggingSystemMock, elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; +import { actionsClientMock } from '../../../actions/server/mocks'; +import { + CaseService, + CaseConfigureService, + CaseUserActionServiceSetup, + AlertService, +} from '../services'; import { CaseClient } from './types'; import { authenticationMock } from '../routes/api/__fixtures__'; import { createCaseClient } from '.'; +import { getActions } from '../routes/api/__mocks__/request_responses'; export type CaseClientMock = jest.Mocked; export const createCaseClientMock = (): CaseClientMock => ({ create: jest.fn(), update: jest.fn(), addComment: jest.fn(), + updateAlertsStatus: jest.fn(), }); export const createCaseClientWithMockSavedObjectsClient = async ( @@ -25,7 +33,10 @@ export const createCaseClientWithMockSavedObjectsClient = async ( client: CaseClient; services: { userActionService: jest.Mocked }; }> => { + const actionsMock = actionsClientMock.create(); + actionsMock.getAll.mockImplementation(() => Promise.resolve(getActions())); const log = loggingSystemMock.create().get('case'); + const esClientMock = elasticsearchServiceMock.createClusterClient(); const request = {} as KibanaRequest; const caseServicePlugin = new CaseService(log); @@ -39,15 +50,38 @@ export const createCaseClientWithMockSavedObjectsClient = async ( postUserActions: jest.fn(), getUserActions: jest.fn(), }; + const alertsService = new AlertService(); + alertsService.initialize(esClientMock); + + const context = ({ + core: { + savedObjects: { + client: savedObjectsClient, + }, + }, + actions: { getActionsClient: () => actionsMock }, + case: { + getCaseClient: () => caseClient, + }, + securitySolution: { + getAppClient: () => ({ + getSignalsIndex: () => '.siem-signals', + }), + }, + } as unknown) as RequestHandlerContext; + + const caseClient = createCaseClient({ + savedObjectsClient, + request, + caseService, + caseConfigureService, + userActionService, + alertsService, + context, + }); return { - client: createCaseClient({ - savedObjectsClient, - request, - caseService, - caseConfigureService, - userActionService, - }), + client: caseClient, services: { userActionService }, }; }; diff --git a/x-pack/plugins/case/server/client/types.ts b/x-pack/plugins/case/server/client/types.ts index 8db7d8a5747d7..a9e8494c43dbc 100644 --- a/x-pack/plugins/case/server/client/types.ts +++ b/x-pack/plugins/case/server/client/types.ts @@ -4,18 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest, SavedObjectsClientContract } from '../../../../../src/core/server'; +import { KibanaRequest, SavedObjectsClientContract, RequestHandlerContext } from 'kibana/server'; import { CasePostRequest, CasesPatchRequest, CommentRequest, CaseResponse, CasesResponse, + CaseStatuses, } from '../../common/api'; import { CaseConfigureServiceSetup, CaseServiceSetup, CaseUserActionServiceSetup, + AlertServiceContract, } from '../services'; export interface CaseClientCreate { @@ -23,24 +25,36 @@ export interface CaseClientCreate { } export interface CaseClientUpdate { + caseClient: CaseClient; cases: CasesPatchRequest; } export interface CaseClientAddComment { + caseClient: CaseClient; caseId: string; comment: CommentRequest; } +export interface CaseClientUpdateAlertsStatus { + ids: string[]; + status: CaseStatuses; +} + +type PartialExceptFor = Partial & Pick; + export interface CaseClientFactoryArguments { savedObjectsClient: SavedObjectsClientContract; request: KibanaRequest; caseConfigureService: CaseConfigureServiceSetup; caseService: CaseServiceSetup; userActionService: CaseUserActionServiceSetup; + alertsService: AlertServiceContract; + context?: PartialExceptFor; } export interface CaseClient { create: (args: CaseClientCreate) => Promise; update: (args: CaseClientUpdate) => Promise; addComment: (args: CaseClientAddComment) => Promise; + updateAlertsStatus: (args: CaseClientUpdateAlertsStatus) => Promise; } diff --git a/x-pack/plugins/case/server/connectors/case/index.test.ts b/x-pack/plugins/case/server/connectors/case/index.test.ts index adf94661216cb..9f5b186c0c687 100644 --- a/x-pack/plugins/case/server/connectors/case/index.test.ts +++ b/x-pack/plugins/case/server/connectors/case/index.test.ts @@ -14,6 +14,7 @@ import { createCaseServiceMock, createConfigureServiceMock, createUserActionServiceMock, + createAlertServiceMock, } from '../../services/mocks'; import { CaseActionType, CaseActionTypeExecutorOptions, CaseExecutorParams } from './types'; import { getActionType } from '.'; @@ -35,11 +36,13 @@ describe('case connector', () => { const caseService = createCaseServiceMock(); const caseConfigureService = createConfigureServiceMock(); const userActionService = createUserActionServiceMock(); + const alertsService = createAlertServiceMock(); caseActionType = getActionType({ logger, caseService, caseConfigureService, userActionService, + alertsService, }); }); @@ -62,6 +65,9 @@ describe('case connector', () => { parent: null, }, }, + settings: { + syncAlerts: true, + }, }, }; @@ -98,6 +104,9 @@ describe('case connector', () => { parent: null, }, }, + settings: { + syncAlerts: true, + }, }, }, }, @@ -118,6 +127,9 @@ describe('case connector', () => { severityCode: '3', }, }, + settings: { + syncAlerts: true, + }, }, }, }, @@ -139,6 +151,9 @@ describe('case connector', () => { urgency: 'Medium', }, }, + settings: { + syncAlerts: true, + }, }, }, }, @@ -156,6 +171,9 @@ describe('case connector', () => { type: '.none', fields: null, }, + settings: { + syncAlerts: true, + }, }, }, }, @@ -180,6 +198,9 @@ describe('case connector', () => { type: '.servicenow', fields: {}, }, + settings: { + syncAlerts: true, + }, }, }; @@ -195,6 +216,9 @@ describe('case connector', () => { type: '.servicenow', fields: { impact: null, severity: null, urgency: null }, }, + settings: { + syncAlerts: true, + }, }, }); }); @@ -212,6 +236,9 @@ describe('case connector', () => { type: '.none', fields: null, }, + settings: { + syncAlerts: true, + }, }, }; @@ -234,6 +261,9 @@ describe('case connector', () => { parent: null, }, }, + settings: { + syncAlerts: true, + }, }, }; @@ -262,6 +292,9 @@ describe('case connector', () => { excess: null, }, }, + settings: { + syncAlerts: true, + }, }, }; @@ -289,6 +322,9 @@ describe('case connector', () => { parent: null, }, }, + settings: { + syncAlerts: true, + }, }, }; @@ -312,6 +348,9 @@ describe('case connector', () => { type: '.none', fields: {}, }, + settings: { + syncAlerts: true, + }, }, }; @@ -343,6 +382,7 @@ describe('case connector', () => { title: null, status: null, connector: null, + settings: null, ...(params.subActionParams as Record), }, }); @@ -375,6 +415,7 @@ describe('case connector', () => { tags: null, title: null, status: null, + settings: null, ...(params.subActionParams as Record), }, }); @@ -405,6 +446,7 @@ describe('case connector', () => { tags: null, title: null, status: null, + settings: null, ...(params.subActionParams as Record), }, }); @@ -436,6 +478,7 @@ describe('case connector', () => { tags: null, title: null, status: null, + settings: null, ...(params.subActionParams as Record), }, }); @@ -465,6 +508,7 @@ describe('case connector', () => { tags: null, title: null, status: null, + settings: null, connector: { id: 'servicenow', name: 'Servicenow', @@ -497,6 +541,7 @@ describe('case connector', () => { tags: null, title: null, status: null, + settings: null, ...(params.subActionParams as Record), }, }); @@ -630,7 +675,9 @@ describe('case connector', () => { expect(validateParams(caseActionType, params)).toEqual(params); }); - it('succeeds when type is an alert', () => { + // TODO: Enable when the creation of comments of type alert is supported + // https://github.com/elastic/kibana/issues/85750 + it.skip('succeeds when type is an alert', () => { const params: Record = { subAction: 'addComment', subActionParams: { @@ -656,6 +703,26 @@ describe('case connector', () => { }).toThrow(); }); + // TODO: Remove it when the creation of comments of type alert is supported + // https://github.com/elastic/kibana/issues/85750 + it('fails when type is an alert', () => { + const params: Record = { + subAction: 'addComment', + subActionParams: { + caseId: 'case-id', + comment: { + type: CommentType.alert, + alertId: 'test-id', + index: 'test-index', + }, + }, + }; + + expect(() => { + validateParams(caseActionType, params); + }).toThrow(); + }); + it('fails when missing attributes: type user', () => { const allParams = { type: CommentType.user, @@ -678,7 +745,9 @@ describe('case connector', () => { }); }); - it('fails when missing attributes: type alert', () => { + // TODO: Enable when the creation of comments of type alert is supported + // https://github.com/elastic/kibana/issues/85750 + it.skip('fails when missing attributes: type alert', () => { const allParams = { type: CommentType.alert, comment: 'a comment', @@ -720,7 +789,9 @@ describe('case connector', () => { }); }); - it('fails when excess attributes are provided: type alert', () => { + // TODO: Enable when the creation of comments of type alert is supported + // https://github.com/elastic/kibana/issues/85750 + it.skip('fails when excess attributes are provided: type alert', () => { ['comment'].forEach((attribute) => { const params: Record = { subAction: 'addComment', @@ -789,6 +860,9 @@ describe('case connector', () => { updated_at: null, updated_by: null, version: 'WzksMV0=', + settings: { + syncAlerts: true, + }, }; mockCaseClient.create.mockReturnValue(Promise.resolve(createReturn)); @@ -810,6 +884,9 @@ describe('case connector', () => { parent: null, }, }, + settings: { + syncAlerts: true, + }, }, }; @@ -879,6 +956,9 @@ describe('case connector', () => { username: 'awesome', }, version: 'WzE3LDFd', + settings: { + syncAlerts: true, + }, }, ]; @@ -895,6 +975,7 @@ describe('case connector', () => { tags: null, status: null, connector: null, + settings: null, }, }; @@ -910,6 +991,7 @@ describe('case connector', () => { expect(result).toEqual({ actionId, status: 'ok', data: updateReturn }); expect(mockCaseClient.update).toHaveBeenCalledWith({ + caseClient: mockCaseClient, // Null values have been striped out. cases: { cases: [ @@ -960,6 +1042,9 @@ describe('case connector', () => { version: 'WzksMV0=', }, ], + settings: { + syncAlerts: true, + }, }; mockCaseClient.addComment.mockReturnValue(Promise.resolve(commentReturn)); @@ -988,6 +1073,7 @@ describe('case connector', () => { expect(result).toEqual({ actionId, status: 'ok', data: commentReturn }); expect(mockCaseClient.addComment).toHaveBeenCalledWith({ + caseClient: mockCaseClient, caseId: 'case-id', comment: { comment: 'a comment', diff --git a/x-pack/plugins/case/server/connectors/case/index.ts b/x-pack/plugins/case/server/connectors/case/index.ts index dc647d288ec65..48124b8ae32eb 100644 --- a/x-pack/plugins/case/server/connectors/case/index.ts +++ b/x-pack/plugins/case/server/connectors/case/index.ts @@ -6,7 +6,7 @@ import { curry } from 'lodash'; -import { KibanaRequest } from 'kibana/server'; +import { KibanaRequest, RequestHandlerContext } from 'kibana/server'; import { ActionTypeExecutorResult } from '../../../../actions/common'; import { CasePatchRequest, CasePostRequest } from '../../../common/api'; import { createCaseClient } from '../../client'; @@ -30,6 +30,7 @@ export function getActionType({ caseService, caseConfigureService, userActionService, + alertsService, }: GetActionTypeParams): CaseActionType { return { id: CASE_ACTION_TYPE_ID, @@ -39,13 +40,25 @@ export function getActionType({ config: CaseConfigurationSchema, params: CaseExecutorParamsSchema, }, - executor: curry(executor)({ logger, caseService, caseConfigureService, userActionService }), + executor: curry(executor)({ + logger, + caseService, + caseConfigureService, + userActionService, + alertsService, + }), }; } // action executor async function executor( - { logger, caseService, caseConfigureService, userActionService }: GetActionTypeParams, + { + logger, + caseService, + caseConfigureService, + userActionService, + alertsService, + }: GetActionTypeParams, execOptions: CaseActionTypeExecutorOptions ): Promise> { const { actionId, params, services } = execOptions; @@ -59,6 +72,9 @@ async function executor( caseService, caseConfigureService, userActionService, + alertsService, + // TODO: When case connector is enabled we should figure out how to pass the context. + context: {} as RequestHandlerContext, }); if (!supportedSubActions.includes(subAction)) { @@ -80,12 +96,15 @@ async function executor( {} as CasePatchRequest ); - data = await caseClient.update({ cases: { cases: [updateParamsWithoutNullValues] } }); + data = await caseClient.update({ + caseClient, + cases: { cases: [updateParamsWithoutNullValues] }, + }); } if (subAction === 'addComment') { const { caseId, comment } = subActionParams as ExecutorSubActionAddCommentParams; - data = await caseClient.addComment({ caseId, comment }); + data = await caseClient.addComment({ caseClient, caseId, comment }); } return { status: 'ok', data: data ?? {}, actionId }; diff --git a/x-pack/plugins/case/server/connectors/case/schema.ts b/x-pack/plugins/case/server/connectors/case/schema.ts index 039c0e2e7e67f..d17c9ce6eb1cc 100644 --- a/x-pack/plugins/case/server/connectors/case/schema.ts +++ b/x-pack/plugins/case/server/connectors/case/schema.ts @@ -14,13 +14,27 @@ const ContextTypeUserSchema = schema.object({ comment: schema.string(), }); -const ContextTypeAlertSchema = schema.object({ - type: schema.literal('alert'), - alertId: schema.string(), - index: schema.string(), -}); - -export const CommentSchema = schema.oneOf([ContextTypeUserSchema, ContextTypeAlertSchema]); +/** + * ContextTypeAlertSchema has been deleted. + * Comments of type alert need the siem signal index. + * Case connector is not being passed the context which contains the + * security solution app client which in turn provides the siem signal index. + * For that reason, we disable comments of type alert for the case connector until + * we figure out how to pass the security solution app client to the connector. + * See: x-pack/plugins/case/server/connectors/case/index.ts L76. + * + * The schema: + * + * const ContextTypeAlertSchema = schema.object({ + * type: schema.literal('alert'), + * alertId: schema.string(), + * index: schema.string(), + * }); + * + * Issue: https://github.com/elastic/kibana/issues/85750 + * */ + +export const CommentSchema = schema.oneOf([ContextTypeUserSchema]); const JiraFieldsSchema = schema.object({ issueType: schema.string(), @@ -80,6 +94,7 @@ const CaseBasicProps = { title: schema.string(), tags: schema.arrayOf(schema.string()), connector: schema.object(ConnectorProps, { validate: validateConnector }), + settings: schema.object({ syncAlerts: schema.boolean() }), }; const CaseUpdateRequestProps = { @@ -89,6 +104,7 @@ const CaseUpdateRequestProps = { title: schema.nullable(CaseBasicProps.title), tags: schema.nullable(CaseBasicProps.tags), connector: schema.nullable(CaseBasicProps.connector), + settings: schema.nullable(CaseBasicProps.settings), status: schema.nullable(schema.string()), }; diff --git a/x-pack/plugins/case/server/connectors/index.ts b/x-pack/plugins/case/server/connectors/index.ts index bee7b1e475457..f373445719164 100644 --- a/x-pack/plugins/case/server/connectors/index.ts +++ b/x-pack/plugins/case/server/connectors/index.ts @@ -16,6 +16,7 @@ import { CaseServiceSetup, CaseConfigureServiceSetup, CaseUserActionServiceSetup, + AlertServiceContract, } from '../services'; import { getActionType as getCaseConnector } from './case'; @@ -26,6 +27,7 @@ export interface GetActionTypeParams { caseService: CaseServiceSetup; caseConfigureService: CaseConfigureServiceSetup; userActionService: CaseUserActionServiceSetup; + alertsService: AlertServiceContract; } export interface RegisterConnectorsArgs extends GetActionTypeParams { @@ -45,6 +47,7 @@ export const registerConnectors = ({ caseService, caseConfigureService, userActionService, + alertsService, }: RegisterConnectorsArgs) => { actionsRegisterType( getCaseConnector({ @@ -52,6 +55,7 @@ export const registerConnectors = ({ caseService, caseConfigureService, userActionService, + alertsService, }) ); }; diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 64c4b422d1cf7..8d508ce0b76b1 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -11,6 +11,7 @@ import { Logger, PluginInitializerContext, RequestHandler, + RequestHandlerContext, } from 'kibana/server'; import { CoreSetup, CoreStart } from 'src/core/server'; @@ -33,6 +34,8 @@ import { CaseServiceSetup, CaseUserActionService, CaseUserActionServiceSetup, + AlertService, + AlertServiceContract, } from './services'; import { createCaseClient } from './client'; import { registerConnectors } from './connectors'; @@ -51,6 +54,7 @@ export class CasePlugin { private caseService?: CaseServiceSetup; private caseConfigureService?: CaseConfigureServiceSetup; private userActionService?: CaseUserActionServiceSetup; + private alertsService?: AlertService; constructor(private readonly initializerContext: PluginInitializerContext) { this.log = this.initializerContext.logger.get(); @@ -79,6 +83,7 @@ export class CasePlugin { }); this.caseConfigureService = await new CaseConfigureService(this.log).setup(); this.userActionService = await new CaseUserActionService(this.log).setup(); + this.alertsService = new AlertService(); core.http.registerRouteHandlerContext( APP_ID, @@ -87,6 +92,7 @@ export class CasePlugin { caseService: this.caseService, caseConfigureService: this.caseConfigureService, userActionService: this.userActionService, + alertsService: this.alertsService, }) ); @@ -104,24 +110,31 @@ export class CasePlugin { caseService: this.caseService, caseConfigureService: this.caseConfigureService, userActionService: this.userActionService, + alertsService: this.alertsService, }); } public async start(core: CoreStart) { this.log.debug(`Starting Case Workflow`); + this.alertsService!.initialize(core.elasticsearch.client); - const getCaseClientWithRequest = async (request: KibanaRequest) => { + const getCaseClientWithRequestAndContext = async ( + context: RequestHandlerContext, + request: KibanaRequest + ) => { return createCaseClient({ savedObjectsClient: core.savedObjects.getScopedClient(request), request, caseService: this.caseService!, caseConfigureService: this.caseConfigureService!, userActionService: this.userActionService!, + alertsService: this.alertsService!, + context, }); }; return { - getCaseClientWithRequest, + getCaseClientWithRequestAndContext, }; } @@ -134,11 +147,13 @@ export class CasePlugin { caseService, caseConfigureService, userActionService, + alertsService, }: { core: CoreSetup; caseService: CaseServiceSetup; caseConfigureService: CaseConfigureServiceSetup; userActionService: CaseUserActionServiceSetup; + alertsService: AlertServiceContract; }): IContextProvider, typeof APP_ID> => { return async (context, request) => { const [{ savedObjects }] = await core.getStartServices(); @@ -149,7 +164,9 @@ export class CasePlugin { caseService, caseConfigureService, userActionService, + alertsService, request, + context, }); }, }; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts index 95856dd75d0ae..645673fdee756 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -44,6 +44,9 @@ export const mockCases: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + settings: { + syncAlerts: true, + }, }, references: [], updated_at: '2019-11-25T21:54:48.952Z', @@ -78,6 +81,9 @@ export const mockCases: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + settings: { + syncAlerts: true, + }, }, references: [], updated_at: '2019-11-25T22:32:00.900Z', @@ -116,6 +122,9 @@ export const mockCases: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + settings: { + syncAlerts: true, + }, }, references: [], updated_at: '2019-11-25T22:32:17.947Z', @@ -158,6 +167,9 @@ export const mockCases: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + settings: { + syncAlerts: true, + }, }, references: [], updated_at: '2019-11-25T22:32:17.947Z', @@ -188,6 +200,9 @@ export const mockCaseNoConnectorId: SavedObject> = { email: 'testemail@elastic.co', username: 'elastic', }, + settings: { + syncAlerts: true, + }, }, references: [], updated_at: '2019-11-25T21:54:48.952Z', diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts index 67890599fa417..dcae1c6083eb6 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts @@ -5,10 +5,10 @@ */ import { RequestHandlerContext, KibanaRequest } from 'src/core/server'; -import { loggingSystemMock } from 'src/core/server/mocks'; +import { loggingSystemMock, elasticsearchServiceMock } from 'src/core/server/mocks'; import { actionsClientMock } from '../../../../../actions/server/mocks'; import { createCaseClient } from '../../../client'; -import { CaseService, CaseConfigureService } from '../../../services'; +import { CaseService, CaseConfigureService, AlertService } from '../../../services'; import { getActions } from '../__mocks__/request_responses'; import { authenticationMock } from '../__fixtures__'; @@ -16,6 +16,7 @@ export const createRouteContext = async (client: any, badAuth = false) => { const actionsMock = actionsClientMock.create(); actionsMock.getAll.mockImplementation(() => Promise.resolve(getActions())); const log = loggingSystemMock.create().get('case'); + const esClientMock = elasticsearchServiceMock.createClusterClient(); const caseServicePlugin = new CaseService(log); const caseConfigureServicePlugin = new CaseConfigureService(log); @@ -24,18 +25,10 @@ export const createRouteContext = async (client: any, badAuth = false) => { authentication: badAuth ? authenticationMock.createInvalid() : authenticationMock.create(), }); const caseConfigureService = await caseConfigureServicePlugin.setup(); - const caseClient = createCaseClient({ - savedObjectsClient: client, - request: {} as KibanaRequest, - caseService, - caseConfigureService, - userActionService: { - postUserActions: jest.fn(), - getUserActions: jest.fn(), - }, - }); + const alertsService = new AlertService(); + alertsService.initialize(esClientMock); - return ({ + const context = ({ core: { savedObjects: { client, @@ -45,5 +38,25 @@ export const createRouteContext = async (client: any, badAuth = false) => { case: { getCaseClient: () => caseClient, }, + securitySolution: { + getAppClient: () => ({ + getSignalsIndex: () => '.siem-signals', + }), + }, } as unknown) as RequestHandlerContext; + + const caseClient = createCaseClient({ + savedObjectsClient: client, + request: {} as KibanaRequest, + caseService, + caseConfigureService, + userActionService: { + postUserActions: jest.fn(), + getUserActions: jest.fn(), + }, + alertsService, + context, + }); + + return context; }; diff --git a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts index ce35b99750419..209fa11116c56 100644 --- a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts +++ b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts @@ -17,6 +17,9 @@ export const newCase: CasePostRequest = { type: ConnectorTypes.none, fields: null, }, + settings: { + syncAlerts: true, + }, }; export const getActions = (): FindActionResult[] => [ diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts index 08d442bccf2cb..139fb7c5f27a4 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts @@ -32,7 +32,7 @@ export function initPostCommentApi({ router }: RouteDeps) { try { return response.ok({ - body: await caseClient.addComment({ caseId, comment }), + body: await caseClient.addComment({ caseClient, caseId, comment }), }); } catch (error) { return response.customError(wrapError(error)); diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts index 053f9ec18ab0f..6a6f5653375b8 100644 --- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts @@ -74,6 +74,9 @@ describe('PATCH cases', () => { updated_at: '2019-11-25T21:54:48.952Z', updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, version: 'WzE3LDFd', + settings: { + syncAlerts: true, + }, }, ]); }); @@ -125,6 +128,9 @@ describe('PATCH cases', () => { updated_at: '2019-11-25T21:54:48.952Z', updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, version: 'WzE3LDFd', + settings: { + syncAlerts: true, + }, }, ]); }); @@ -175,6 +181,9 @@ describe('PATCH cases', () => { updated_at: '2019-11-25T21:54:48.952Z', updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, version: 'WzE3LDFd', + settings: { + syncAlerts: true, + }, }, ]); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts index 873671a909801..178e40520d9d2 100644 --- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts @@ -27,7 +27,7 @@ export function initPatchCasesApi({ router }: RouteDeps) { try { return response.ok({ - body: await caseClient.update({ cases }), + body: await caseClient.update({ caseClient, cases }), }); } catch (error) { return response.customError(wrapError(error)); diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts index 508684b422891..ea59959b0e849 100644 --- a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts @@ -42,6 +42,9 @@ describe('POST cases', () => { type: ConnectorTypes.none, fields: null, }, + settings: { + syncAlerts: true, + }, }, }); @@ -78,6 +81,9 @@ describe('POST cases', () => { type: '.jira', fields: { issueType: 'Task', priority: 'High', parent: null }, }, + settings: { + syncAlerts: true, + }, }, }); @@ -108,6 +114,9 @@ describe('POST cases', () => { status: CaseStatuses.open, tags: ['defacement'], connector: null, + settings: { + syncAlerts: true, + }, }, }); @@ -130,6 +139,9 @@ describe('POST cases', () => { title: 'Super Bad Security Issue', tags: ['error'], connector: null, + settings: { + syncAlerts: true, + }, }, }); @@ -160,6 +172,9 @@ describe('POST cases', () => { type: ConnectorTypes.none, fields: null, }, + settings: { + syncAlerts: true, + }, }, }); @@ -199,6 +214,9 @@ describe('POST cases', () => { updated_at: null, updated_by: null, version: 'WzksMV0=', + settings: { + syncAlerts: true, + }, }); }); }); diff --git a/x-pack/plugins/case/server/routes/api/utils.test.ts b/x-pack/plugins/case/server/routes/api/utils.test.ts index 7654ae5ff0d1a..405da0df17542 100644 --- a/x-pack/plugins/case/server/routes/api/utils.test.ts +++ b/x-pack/plugins/case/server/routes/api/utils.test.ts @@ -302,6 +302,9 @@ describe('Utils', () => { comments: [], totalComment: 2, version: 'WzAsMV0=', + settings: { + syncAlerts: true, + }, }, ]); }); @@ -341,6 +344,9 @@ describe('Utils', () => { comments: [], totalComment: 0, version: 'WzAsMV0=', + settings: { + syncAlerts: true, + }, }, ]); }); @@ -387,6 +393,9 @@ describe('Utils', () => { comments: [], totalComment: 0, version: 'WzAsMV0=', + settings: { + syncAlerts: true, + }, }, ]); }); @@ -497,6 +506,9 @@ describe('Utils', () => { comments: [], totalComment: 2, version: 'WzAsMV0=', + settings: { + syncAlerts: true, + }, }); }); }); diff --git a/x-pack/plugins/case/server/saved_object_types/cases.ts b/x-pack/plugins/case/server/saved_object_types/cases.ts index d8ee2f90f3d93..6468d4b3aa61d 100644 --- a/x-pack/plugins/case/server/saved_object_types/cases.ts +++ b/x-pack/plugins/case/server/saved_object_types/cases.ts @@ -134,6 +134,13 @@ export const caseSavedObjectType: SavedObjectsType = { }, }, }, + settings: { + properties: { + syncAlerts: { + type: 'boolean', + }, + }, + }, }, }, migrations: caseMigrations, diff --git a/x-pack/plugins/case/server/saved_object_types/migrations.ts b/x-pack/plugins/case/server/saved_object_types/migrations.ts index 27c363a40af37..9124314ac3f5e 100644 --- a/x-pack/plugins/case/server/saved_object_types/migrations.ts +++ b/x-pack/plugins/case/server/saved_object_types/migrations.ts @@ -9,16 +9,16 @@ import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc } from '../../../../../src/core/server'; import { ConnectorTypes, CommentType } from '../../common/api'; -interface UnsanitizedCase { +interface UnsanitizedCaseConnector { connector_id: string; } -interface UnsanitizedConfigure { +interface UnsanitizedConfigureConnector { connector_id: string; connector_name: string; } -interface SanitizedCase { +interface SanitizedCaseConnector { connector: { id: string; name: string | null; @@ -27,7 +27,7 @@ interface SanitizedCase { }; } -interface SanitizedConfigure { +interface SanitizedConfigureConnector { connector: { id: string; name: string | null; @@ -42,10 +42,16 @@ interface UserActions { old_value: string; } +interface SanitizedCaseSettings { + settings: { + syncAlerts: boolean; + }; +} + export const caseMigrations = { '7.10.0': ( - doc: SavedObjectUnsanitizedDoc - ): SavedObjectSanitizedDoc => { + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { const { connector_id, ...attributesWithoutConnectorId } = doc.attributes; return { @@ -62,12 +68,26 @@ export const caseMigrations = { references: doc.references || [], }; }, + '7.11.0': ( + doc: SavedObjectUnsanitizedDoc> + ): SavedObjectSanitizedDoc => { + return { + ...doc, + attributes: { + ...doc.attributes, + settings: { + syncAlerts: true, + }, + }, + references: doc.references || [], + }; + }, }; export const configureMigrations = { '7.10.0': ( - doc: SavedObjectUnsanitizedDoc - ): SavedObjectSanitizedDoc => { + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { const { connector_id, connector_name, ...restAttributes } = doc.attributes; return { diff --git a/x-pack/plugins/case/server/services/alerts/index.ts b/x-pack/plugins/case/server/services/alerts/index.ts new file mode 100644 index 0000000000000..4fb98278b8afa --- /dev/null +++ b/x-pack/plugins/case/server/services/alerts/index.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import type { PublicMethodsOf } from '@kbn/utility-types'; + +import { IClusterClient, KibanaRequest } from 'kibana/server'; +import { CaseStatuses } from '../../../common/api'; + +export type AlertServiceContract = PublicMethodsOf; + +interface UpdateAlertsStatusArgs { + request: KibanaRequest; + ids: string[]; + status: CaseStatuses; + index: string; +} + +export class AlertService { + private isInitialized = false; + private esClient?: IClusterClient; + + constructor() {} + + public initialize(esClient: IClusterClient) { + if (this.isInitialized) { + throw new Error('AlertService already initialized'); + } + + this.isInitialized = true; + this.esClient = esClient; + } + + public async updateAlertsStatus({ request, ids, status, index }: UpdateAlertsStatusArgs) { + if (!this.isInitialized) { + throw new Error('AlertService not initialized'); + } + + // The above check makes sure that esClient is defined. + const result = await this.esClient!.asScoped(request).asCurrentUser.updateByQuery({ + index, + conflicts: 'abort', + body: { + script: { + source: `ctx._source.signal.status = '${status}'`, + lang: 'painless', + }, + query: { ids: { values: ids } }, + }, + ignore_unavailable: true, + }); + + return result; + } +} diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index 0ce2b196af471..95bcf87361e07 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -31,6 +31,7 @@ import { readTags } from './tags/read_tags'; export { CaseConfigureService, CaseConfigureServiceSetup } from './configure'; export { CaseUserActionService, CaseUserActionServiceSetup } from './user_actions'; +export { AlertService, AlertServiceContract } from './alerts'; export interface ClientArgs { client: SavedObjectsClientContract; diff --git a/x-pack/plugins/case/server/services/mocks.ts b/x-pack/plugins/case/server/services/mocks.ts index 287f80a60ab07..01a8cb09ac2d5 100644 --- a/x-pack/plugins/case/server/services/mocks.ts +++ b/x-pack/plugins/case/server/services/mocks.ts @@ -4,11 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CaseConfigureServiceSetup, CaseServiceSetup, CaseUserActionServiceSetup } from '.'; +import { + CaseConfigureServiceSetup, + CaseServiceSetup, + CaseUserActionServiceSetup, + AlertServiceContract, +} from '.'; export type CaseServiceMock = jest.Mocked; export type CaseConfigureServiceMock = jest.Mocked; export type CaseUserActionServiceMock = jest.Mocked; +export type AlertServiceMock = jest.Mocked; export const createCaseServiceMock = (): CaseServiceMock => ({ deleteCase: jest.fn(), @@ -41,3 +47,8 @@ export const createUserActionServiceMock = (): CaseUserActionServiceMock => ({ getUserActions: jest.fn(), postUserActions: jest.fn(), }); + +export const createAlertServiceMock = (): AlertServiceMock => ({ + initialize: jest.fn(), + updateAlertsStatus: jest.fn(), +}); diff --git a/x-pack/plugins/case/server/services/user_actions/helpers.ts b/x-pack/plugins/case/server/services/user_actions/helpers.ts index c9339862b8f24..c7bdc8b10b5a3 100644 --- a/x-pack/plugins/case/server/services/user_actions/helpers.ts +++ b/x-pack/plugins/case/server/services/user_actions/helpers.ts @@ -129,6 +129,7 @@ const userActionFieldsAllowed: UserActionField = [ 'tags', 'title', 'status', + 'settings', ]; export const buildCaseUserActions = ({ diff --git a/x-pack/plugins/case/server/types.ts b/x-pack/plugins/case/server/types.ts index b95060ef30452..d0dfc26aa7b8c 100644 --- a/x-pack/plugins/case/server/types.ts +++ b/x-pack/plugins/case/server/types.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AppRequestContext } from '../../security_solution/server/types'; import { CaseClient } from './client'; export interface CaseRequestContext { @@ -13,5 +15,8 @@ export interface CaseRequestContext { declare module 'src/core/server' { interface RequestHandlerContext { case?: CaseRequestContext; + // TODO: Remove when triggers_ui do not import case's types. + // PR https://github.com/elastic/kibana/pull/84587. + securitySolution?: AppRequestContext; } } diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx index 755dde9341dca..78bb3a8d2f2f3 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx @@ -474,6 +474,9 @@ describe('AllCases', () => { username: 'lknope', }, version: 'WzQ3LDFd', + settings: { + syncAlerts: true, + }, }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx index 945458e92bc8a..62ce0cc2cc2f5 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; +import React, { useMemo, useCallback } from 'react'; import styled, { css } from 'styled-components'; import { EuiButtonEmpty, @@ -13,6 +13,7 @@ import { EuiDescriptionListTitle, EuiFlexGroup, EuiFlexItem, + EuiIconTip, } from '@elastic/eui'; import { CaseStatuses } from '../../../../../case/common/api'; import * as i18n from '../case_view/translations'; @@ -22,6 +23,8 @@ import { Case } from '../../containers/types'; import { CaseService } from '../../containers/use_get_case_user_actions'; import { StatusContextMenu } from './status_context_menu'; import { getStatusDate, getStatusTitle } from './helpers'; +import { SyncAlertsSwitch } from '../case_settings/sync_alerts_switch'; +import { OnUpdateFields } from '../case_view'; const MyDescriptionList = styled(EuiDescriptionList)` ${({ theme }) => css` @@ -38,7 +41,7 @@ interface CaseActionBarProps { disabled?: boolean; isLoading: boolean; onRefresh: () => void; - onStatusChanged: (status: CaseStatuses) => void; + onUpdateField: (args: OnUpdateFields) => void; } const CaseActionBarComponent: React.FC = ({ caseData, @@ -46,10 +49,27 @@ const CaseActionBarComponent: React.FC = ({ disabled = false, isLoading, onRefresh, - onStatusChanged, + onUpdateField, }) => { const date = useMemo(() => getStatusDate(caseData), [caseData]); const title = useMemo(() => getStatusTitle(caseData.status), [caseData.status]); + const onStatusChanged = useCallback( + (status: CaseStatuses) => + onUpdateField({ + key: 'status', + value: status, + }), + [onUpdateField] + ); + + const onSyncAlertsChanged = useCallback( + (syncAlerts: boolean) => + onUpdateField({ + key: 'settings', + value: { ...caseData.settings, syncAlerts }, + }), + [caseData.settings, onUpdateField] + ); return ( @@ -78,20 +98,41 @@ const CaseActionBarComponent: React.FC = ({ - - - - {i18n.CASE_REFRESH} - - - - - - + + + + + + + + {i18n.STATUS} + + + + + + + + + + + + {i18n.CASE_REFRESH} + + + + + + + ); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_settings/sync_alerts_switch.tsx b/x-pack/plugins/security_solution/public/cases/components/case_settings/sync_alerts_switch.tsx new file mode 100644 index 0000000000000..ab91f2ae8cdf3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/case_settings/sync_alerts_switch.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useCallback, useState } from 'react'; +import { EuiSwitch } from '@elastic/eui'; + +import * as i18n from '../../translations'; + +interface Props { + disabled: boolean; + isSynced?: boolean; + showLabel?: boolean; + onSwitchChange?: (isSynced: boolean) => void; +} + +const SyncAlertsSwitchComponent: React.FC = ({ + disabled, + isSynced = true, + showLabel = false, + onSwitchChange, +}) => { + const [isOn, setIsOn] = useState(isSynced); + + const onChange = useCallback(() => { + if (onSwitchChange) { + onSwitchChange(!isOn); + } + + setIsOn(!isOn); + }, [isOn, onSwitchChange]); + + return ( + + ); +}; + +SyncAlertsSwitchComponent.displayName = 'SyncAlertsSwitchComponent'; + +export const SyncAlertsSwitch = memo(SyncAlertsSwitchComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index 0e6226f69fce7..6007038b33ab7 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -16,7 +16,7 @@ import { EuiHorizontalRule, } from '@elastic/eui'; -import { CaseStatuses } from '../../../../../case/common/api'; +import { CaseStatuses, CaseAttributes } from '../../../../../case/common/api'; import { Case, CaseConnector } from '../../containers/types'; import { getCaseDetailsUrl, getCaseUrl, useFormatUrl } from '../../../common/components/link_to'; import { gutterTimeline } from '../../../common/lib/helpers'; @@ -234,6 +234,21 @@ export const CaseComponent = React.memo( onError, }); } + break; + case 'settings': + const settingsUpdate = getTypedPayload(value); + if (caseData.settings !== value) { + updateCaseProperty({ + fetchCaseUserActions, + updateKey: 'settings', + updateValue: settingsUpdate, + updateCase: handleUpdateNewCase, + version: caseData.version, + onSuccess, + onError, + }); + } + break; default: return null; } @@ -397,9 +412,9 @@ export const CaseComponent = React.memo( currentExternalIncident={currentExternalIncident} caseData={caseData} disabled={!userCanCrud} - isLoading={isLoading && updateKey === 'status'} + isLoading={isLoading && (updateKey === 'status' || updateKey === 'settings')} onRefresh={handleRefresh} - onStatusChanged={changeStatus} + onUpdateField={onUpdateField} /> diff --git a/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx b/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx index b2a0f3c351552..67c536f652ec1 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx @@ -7,13 +7,13 @@ import React, { memo, useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { ConnectorTypeFields } from '../../../../../case/common/api/connectors'; import { UseField, useFormData, FieldHook } from '../../../shared_imports'; import { useConnectors } from '../../containers/configure/use_connectors'; import { ConnectorSelector } from '../connector_selector/form'; import { SettingFieldsForm } from '../settings/fields_form'; import { ActionConnector } from '../../containers/types'; import { getConnectorById } from '../configure_cases/utils'; +import { FormProps } from './schema'; interface Props { isLoading: boolean; @@ -21,7 +21,7 @@ interface Props { interface SettingsFieldProps { connectors: ActionConnector[]; - field: FieldHook; + field: FieldHook; isEdit: boolean; } diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form.test.tsx index e64b2b3a05080..3091e6b33d333 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form.test.tsx @@ -25,6 +25,7 @@ const initialCaseValue: FormProps = { title: '', connectorId: 'none', fields: null, + syncAlerts: true, }; describe('CreateCaseForm', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form.tsx index 40db4d792c1c8..308dc63916934 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form.tsx @@ -15,6 +15,7 @@ import { Description } from './description'; import { Tags } from './tags'; import { Connector } from './connector'; import * as i18n from './translations'; +import { SyncAlertsToggle } from './sync_alerts_toggle'; interface ContainerProps { big?: boolean; @@ -61,6 +62,18 @@ export const CreateCaseForm: React.FC = React.memo(({ withSteps = true }) const secondStep = useMemo( () => ({ title: i18n.STEP_TWO_TITLE, + children: ( + + + + ), + }), + [isSubmitting] + ); + + const thirdStep = useMemo( + () => ({ + title: i18n.STEP_THREE_TITLE, children: ( @@ -70,7 +83,11 @@ export const CreateCaseForm: React.FC = React.memo(({ withSteps = true }) [isSubmitting] ); - const allSteps = useMemo(() => [firstStep, secondStep], [firstStep, secondStep]); + const allSteps = useMemo(() => [firstStep, secondStep, thirdStep], [ + firstStep, + secondStep, + thirdStep, + ]); return ( <> @@ -85,6 +102,7 @@ export const CreateCaseForm: React.FC = React.memo(({ withSteps = true }) <> {firstStep.children} {secondStep.children} + {thirdStep.children} )} diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx index e11e508b60ebf..4575059a5a6c0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx @@ -23,6 +23,7 @@ const initialCaseValue: FormProps = { title: '', connectorId: 'none', fields: null, + syncAlerts: true, }; interface Props { @@ -34,14 +35,21 @@ export const FormContext: React.FC = ({ children, onSuccess }) => { const { caseData, postCase } = usePostCase(); const submitCase = useCallback( - async ({ connectorId: dataConnectorId, fields, ...dataWithoutConnectorId }, isValid) => { + async ( + { connectorId: dataConnectorId, fields, syncAlerts, ...dataWithoutConnectorId }, + isValid + ) => { if (isValid) { const caseConnector = getConnectorById(dataConnectorId, connectors); const connectorToUpdate = caseConnector ? normalizeActionConnector(caseConnector, fields) : getNoneConnector(); - await postCase({ ...dataWithoutConnectorId, connector: connectorToUpdate }); + await postCase({ + ...dataWithoutConnectorId, + connector: connectorToUpdate, + settings: { syncAlerts }, + }); } }, [postCase, connectors] diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx index 29073e7774158..fe5b3bea6445c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx @@ -8,8 +8,9 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; import { act, waitFor } from '@testing-library/react'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { TestProviders } from '../../../common/mock'; +import { CasePostRequest } from '../../../../../case/common/api'; +import { TestProviders } from '../../../common/mock'; import { usePostCase } from '../../containers/use_post_case'; import { useGetTags } from '../../containers/use_get_tags'; import { useConnectors } from '../../containers/configure/use_connectors'; @@ -41,7 +42,7 @@ const useGetFieldsByIssueTypeMock = useGetFieldsByIssueType as jest.Mock; const postCase = jest.fn(); const sampleTags = ['coke', 'pepsi']; -const sampleData = { +const sampleData: CasePostRequest = { description: 'what a great description', tags: sampleTags, title: 'what a cool title', @@ -51,6 +52,9 @@ const sampleData = { name: 'none', type: ConnectorTypes.none, }, + settings: { + syncAlerts: true, + }, }; const defaultPostCase = { diff --git a/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx b/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx index a336860121c94..34f0bdd051483 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx @@ -6,7 +6,7 @@ import { CasePostRequest, ConnectorTypeFields } from '../../../../../case/common/api'; import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../shared_imports'; -import * as i18n from '../../translations'; +import * as i18n from './translations'; import { OptionalFieldLabel } from './optional_field_label'; const { emptyField } = fieldValidators; @@ -18,9 +18,10 @@ export const schemaTags = { labelAppend: OptionalFieldLabel, }; -export type FormProps = Omit & { +export type FormProps = Omit & { connectorId: string; fields: ConnectorTypeFields['fields']; + syncAlerts: boolean; }; export const schema: FormSchema = { @@ -47,4 +48,10 @@ export const schema: FormSchema = { label: i18n.CONNECTORS, defaultValue: 'none', }, + fields: {}, + syncAlerts: { + helpText: i18n.SYNC_ALERTS_HELP, + type: FIELD_TYPES.TOGGLE, + defaultValue: true, + }, }; diff --git a/x-pack/plugins/security_solution/public/cases/components/create/sync_alerts_toggle.tsx b/x-pack/plugins/security_solution/public/cases/components/create/sync_alerts_toggle.tsx new file mode 100644 index 0000000000000..0abb2974dd2cb --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/sync_alerts_toggle.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import { Field, getUseField, useFormData } from '../../../shared_imports'; +import * as i18n from './translations'; + +const CommonUseField = getUseField({ component: Field }); + +interface Props { + isLoading: boolean; +} + +const SyncAlertsToggleComponent: React.FC = ({ isLoading }) => { + const [{ syncAlerts }] = useFormData({ watch: ['syncAlerts'] }); + return ( + + ); +}; + +SyncAlertsToggleComponent.displayName = 'SyncAlertsToggleComponent'; + +export const SyncAlertsToggle = memo(SyncAlertsToggleComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/translations.ts b/x-pack/plugins/security_solution/public/cases/components/create/translations.ts index 38916dbddc7d7..f892e080af782 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/create/translations.ts @@ -17,7 +17,21 @@ export const STEP_ONE_TITLE = i18n.translate( export const STEP_TWO_TITLE = i18n.translate( 'xpack.securitySolution.components.create.stepTwoTitle', + { + defaultMessage: 'Case settings', + } +); + +export const STEP_THREE_TITLE = i18n.translate( + 'xpack.securitySolution.components.create.stepThreeTitle', { defaultMessage: 'External Connector Fields', } ); + +export const SYNC_ALERTS_LABEL = i18n.translate( + 'xpack.securitySolution.components.create.syncAlertsLabel', + { + defaultMessage: 'Sync alert status with case status', + } +); diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_alert_comment_event.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_alert_comment_event.tsx index 148ad275b756e..be437073e693c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_alert_comment_event.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_alert_comment_event.tsx @@ -9,7 +9,7 @@ import { EuiLink } from '@elastic/eui'; import { APP_ID } from '../../../../common/constants'; import { useKibana } from '../../../common/lib/kibana'; -import { getRuleDetailsUrl, useFormatUrl } from '../../../common/components/link_to'; +import { getRuleDetailsUrl } from '../../../common/components/link_to'; import { SecurityPageName } from '../../../app/types'; import { Alert } from '../case_view'; @@ -23,16 +23,15 @@ const AlertCommentEventComponent: React.FC = ({ alert }) => { const ruleName = alert?.rule?.name ?? null; const ruleId = alert?.rule?.id ?? null; const { navigateToApp } = useKibana().services.application; - const { formatUrl } = useFormatUrl(SecurityPageName.detections); const onLinkClick = useCallback( (ev: { preventDefault: () => void }) => { ev.preventDefault(); navigateToApp(`${APP_ID}:${SecurityPageName.detections}`, { - path: formatUrl(getRuleDetailsUrl(ruleId ?? '')), + path: getRuleDetailsUrl(ruleId ?? ''), }); }, - [ruleId, formatUrl, navigateToApp] + [ruleId, navigateToApp] ); return ruleId != null && ruleName != null ? ( diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx index f60993fc9aa02..bec1ab3dd4292 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx @@ -384,6 +384,9 @@ describe('Case Configuration API', () => { type: ConnectorTypes.none, fields: null, }, + settings: { + syncAlerts: true, + }, }; test('check url, method, signal', async () => { diff --git a/x-pack/plugins/security_solution/public/cases/containers/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/mock.ts index 40312a8713783..f94fb189c90ce 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/mock.ts @@ -76,6 +76,9 @@ export const basicCase: Case = { updatedAt: basicUpdatedAt, updatedBy: elasticUser, version: 'WzQ3LDFd', + settings: { + syncAlerts: true, + }, }; export const basicCasePost: Case = { diff --git a/x-pack/plugins/security_solution/public/cases/containers/types.ts b/x-pack/plugins/security_solution/public/cases/containers/types.ts index ec1eaa939fe31..a5c9c65dab62a 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/types.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/types.ts @@ -11,6 +11,7 @@ import { CaseConnector, CommentRequest, CaseStatuses, + CaseAttributes, } from '../../../../case/common/api'; export { CaseConnector, ActionConnector } from '../../../../case/common/api'; @@ -63,6 +64,7 @@ export interface Case { updatedAt: string | null; updatedBy: ElasticUser | null; version: string; + settings: CaseAttributes['settings']; } export interface QueryParams { diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx index 44166a14ad292..060ed787c7f4e 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx @@ -74,6 +74,9 @@ export const initialData: Case = { updatedAt: null, updatedBy: null, version: '', + settings: { + syncAlerts: true, + }, }; export interface UseGetCase extends CaseState { diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_case.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_case.test.tsx index c4363236a0977..8e8432d0d190c 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_case.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_case.test.tsx @@ -24,6 +24,9 @@ describe('usePostCase', () => { type: ConnectorTypes.none, fields: null, }, + settings: { + syncAlerts: true, + }, }; beforeEach(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx index c305399ee02d0..08333416d3c46 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx @@ -19,7 +19,7 @@ import { Case } from './types'; export type UpdateKey = keyof Pick< CasePatchRequest, - 'connector' | 'description' | 'status' | 'tags' | 'title' + 'connector' | 'description' | 'status' | 'tags' | 'title' | 'settings' >; interface NewCaseState { diff --git a/x-pack/plugins/security_solution/public/cases/translations.ts b/x-pack/plugins/security_solution/public/cases/translations.ts index a79f7a3af18bf..fd217457f9e7d 100644 --- a/x-pack/plugins/security_solution/public/cases/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/translations.ts @@ -256,3 +256,25 @@ export const IN_PROGRESS_CASES = i18n.translate( defaultMessage: 'In progress cases', } ); + +export const SYNC_ALERTS_SWITCH_LABEL_ON = i18n.translate( + 'xpack.securitySolution.case.settings.syncAlertsSwitchLabelOn', + { + defaultMessage: 'On', + } +); + +export const SYNC_ALERTS_SWITCH_LABEL_OFF = i18n.translate( + 'xpack.securitySolution.case.settings.syncAlertsSwitchLabelOff', + { + defaultMessage: 'Off', + } +); + +export const SYNC_ALERTS_HELP = i18n.translate( + 'xpack.securitySolution.components.create.syncAlertHelpText', + { + defaultMessage: + 'Enabling this option will sync the status of alerts in this case with the case status.', + } +); diff --git a/x-pack/plugins/security_solution/server/index.ts b/x-pack/plugins/security_solution/server/index.ts index 7b84c531dd376..94764fd159360 100644 --- a/x-pack/plugins/security_solution/server/index.ts +++ b/x-pack/plugins/security_solution/server/index.ts @@ -8,6 +8,7 @@ import { PluginInitializerContext, PluginConfigDescriptor } from '../../../../sr import { Plugin, PluginSetup, PluginStart } from './plugin'; import { configSchema, ConfigType } from './config'; import { SIGNALS_INDEX_KEY } from '../common/constants'; +import { AppClient } from './types'; export const plugin = (context: PluginInitializerContext) => { return new Plugin(context); @@ -41,6 +42,7 @@ export const config: PluginConfigDescriptor = { }; export { ConfigType, Plugin, PluginSetup, PluginStart }; +export { AppClient }; // Exports to be shared with plugins such as x-pack/lists plugin export { deleteTemplate } from './lib/detection_engine/index/delete_template'; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/migrations.ts b/x-pack/test/case_api_integration/basic/tests/cases/migrations.ts index 36f07ef92b5f1..df200b34dc429 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/migrations.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/migrations.ts @@ -38,5 +38,18 @@ export default function createGetTests({ getService }: FtrProviderContext) { fields: null, }); }); + + it('7.11.0 migrates cases settings', async () => { + const { body } = await supertest + .get(`${CASES_URL}/e1900ac0-017f-11eb-93f8-d161651bf509`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).key('settings'); + expect(body.settings).to.eql({ + syncAlerts: true, + }); + }); }); } diff --git a/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts index 6949052df4703..ec79c8a1ca494 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts @@ -36,7 +36,7 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); - it(`on new case, user action: 'create' should be called with actionFields: ['description', 'status', 'tags', 'title', 'connector']`, async () => { + it(`on new case, user action: 'create' should be called with actionFields: ['description', 'status', 'tags', 'title', 'connector', 'settings]`, async () => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') @@ -51,7 +51,14 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.length).to.eql(1); - expect(body[0].action_field).to.eql(['description', 'status', 'tags', 'title', 'connector']); + expect(body[0].action_field).to.eql([ + 'description', + 'status', + 'tags', + 'title', + 'connector', + 'settings', + ]); expect(body[0].action).to.eql('create'); expect(body[0].old_value).to.eql(null); expect(body[0].new_value).to.eql(JSON.stringify(postCaseReq)); diff --git a/x-pack/test/case_api_integration/basic/tests/connectors/case.ts b/x-pack/test/case_api_integration/basic/tests/connectors/case.ts index 9a45dd541bb56..e0812d01d0fb8 100644 --- a/x-pack/test/case_api_integration/basic/tests/connectors/case.ts +++ b/x-pack/test/case_api_integration/basic/tests/connectors/case.ts @@ -391,6 +391,9 @@ export default ({ getService }: FtrProviderContext): void => { parent: null, }, }, + settings: { + syncAlerts: true, + }, }, }; @@ -442,6 +445,9 @@ export default ({ getService }: FtrProviderContext): void => { type: '.servicenow', fields: {}, }, + settings: { + syncAlerts: true, + }, }, }; @@ -673,7 +679,53 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - it('should respond with a 400 Bad Request when missing attributes of type alert', async () => { + // TODO: Remove it when the creation of comments of type alert is supported + // https://github.com/elastic/kibana/issues/85750 + it('should fail adding a comment of type alert', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + + const caseRes = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const params = { + subAction: 'addComment', + subActionParams: { + caseId: caseRes.body.id, + comment: { alertId: 'test-id', index: 'test-index', type: CommentType.alert }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/action/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: types that failed validation:\n - [subActionParams.comment.0.type]: expected value to equal [user]', + retry: false, + }); + }); + + // TODO: Enable when the creation of comments of type alert is supported + // https://github.com/elastic/kibana/issues/85750 + it.skip('should respond with a 400 Bad Request when missing attributes of type alert', async () => { const { body: createdAction } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'foo') @@ -754,13 +806,15 @@ export default ({ getService }: FtrProviderContext): void => { expect(caseConnector.body).to.eql({ status: 'error', actionId: createdActionId, - message: `error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: types that failed validation:\n - [subActionParams.comment.0.${attribute}]: definition for this key is missing\n - [subActionParams.comment.1.type]: expected value to equal [alert]`, + message: `error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: types that failed validation:\n - [subActionParams.comment.0.${attribute}]: definition for this key is missing`, retry: false, }); } }); - it('should respond with a 400 Bad Request when adding excess attributes for type alert', async () => { + // TODO: Enable when the creation of comments of type alert is supported + // https://github.com/elastic/kibana/issues/85750 + it.skip('should respond with a 400 Bad Request when adding excess attributes for type alert', async () => { const { body: createdAction } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'foo') @@ -892,7 +946,9 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - it('should add a comment of type alert', async () => { + // TODO: Enable when the creation of comments of type alert is supported + // https://github.com/elastic/kibana/issues/85750 + it.skip('should add a comment of type alert', async () => { const { body: createdAction } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'foo') diff --git a/x-pack/test/case_api_integration/common/lib/mock.ts b/x-pack/test/case_api_integration/common/lib/mock.ts index dac6b2005a9c3..012af6b37f842 100644 --- a/x-pack/test/case_api_integration/common/lib/mock.ts +++ b/x-pack/test/case_api_integration/common/lib/mock.ts @@ -26,6 +26,9 @@ export const postCaseReq: CasePostRequest = { type: '.none' as ConnectorTypes, fields: null, }, + settings: { + syncAlerts: true, + }, }; export const postCommentUserReq: CommentRequestUserType = { From 80ca5a5836e537c2695231b38249a4499d80be5b Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Mon, 14 Dec 2020 14:26:51 -0500 Subject: [PATCH 14/44] [Security Solution][Endpoint][Admin] Custom malware user notification message allows spaces now (#85207) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../policy/store/policy_details/selectors.ts | 38 ++++++++++++++++++- .../view/policy_forms/protections/malware.tsx | 2 +- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts index 7088f094ddcb4..77e975a46d37b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors.ts @@ -31,7 +31,43 @@ export const getPolicyDataForUpdate = ( ): NewPolicyData | Immutable => { // eslint-disable-next-line @typescript-eslint/naming-convention const { id, revision, created_by, created_at, updated_by, updated_at, ...newPolicy } = policy; - return newPolicy; + + // trim custom malware notification string + return { + ...newPolicy, + inputs: (newPolicy as Immutable).inputs.map((input) => ({ + ...input, + config: input.config && { + ...input.config, + policy: { + ...input.config.policy, + value: { + ...input.config.policy.value, + windows: { + ...input.config.policy.value.windows, + popup: { + ...input.config.policy.value.windows.popup, + malware: { + ...input.config.policy.value.windows.popup.malware, + message: input.config.policy.value.windows.popup.malware.message.trim(), + }, + }, + }, + mac: { + ...input.config.policy.value.mac, + popup: { + ...input.config.policy.value.mac.popup, + malware: { + ...input.config.policy.value.mac.popup.malware, + message: input.config.policy.value.mac.popup.malware.message.trim(), + }, + }, + }, + }, + }, + }, + })), + }; }; /** diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index c78455aa8d990..330a0ba407453 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -193,7 +193,7 @@ export const MalwareProtections = React.memo(() => { if (policyDetailsConfig) { const newPayload = cloneDeep(policyDetailsConfig); for (const os of OSes) { - newPayload[os].popup[protection].message = event.target.value.trim(); + newPayload[os].popup[protection].message = event.target.value; } dispatch({ type: 'userChangedPolicyConfig', From 8279c2d1a2979d13cf2a9f5455327287d1b1a0b7 Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Mon, 14 Dec 2020 14:27:58 -0500 Subject: [PATCH 15/44] [Security Solution][Endpoint][Admin] Adds instructional tooltip for malware custom user notification (#85651) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../pages/policy/view/policy_details.test.tsx | 8 +++- .../view/policy_forms/protections/malware.tsx | 40 +++++++++++++++---- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index bfa592b1f9c8e..e9c13b23834b1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -293,7 +293,7 @@ describe('Policy Details', () => { policyView = render(); }); - it('malware popup and message customization options are shown', () => { + it('malware popup, message customization options and tooltip are shown', () => { // use query for finding stuff, if it doesn't find it, just returns null const userNotificationCheckbox = policyView.find( 'EuiCheckbox[data-test-subj="malwareUserNotificationCheckbox"]' @@ -301,8 +301,10 @@ describe('Policy Details', () => { const userNotificationCustomMessageTextArea = policyView.find( 'EuiTextArea[data-test-subj="malwareUserNotificationCustomMessage"]' ); + const tooltip = policyView.find('EuiIconTip'); expect(userNotificationCheckbox).toHaveLength(1); expect(userNotificationCustomMessageTextArea).toHaveLength(1); + expect(tooltip).toHaveLength(1); }); }); describe('when the subscription tier is gold or lower', () => { @@ -311,15 +313,17 @@ describe('Policy Details', () => { policyView = render(); }); - it('malware popup and message customization options are hidden', () => { + it('malware popup, message customization options, and tooltip are hidden', () => { const userNotificationCheckbox = policyView.find( 'EuiCheckbox[data-test-subj="malwareUserNotificationCheckbox"]' ); const userNotificationCustomMessageTextArea = policyView.find( 'EuiTextArea[data-test-subj="malwareUserNotificationCustomMessage"]' ); + const tooltip = policyView.find('EuiIconTip'); expect(userNotificationCheckbox).toHaveLength(0); expect(userNotificationCustomMessageTextArea).toHaveLength(0); + expect(tooltip).toHaveLength(0); }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index 330a0ba407453..d611c4102e8f8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -18,6 +18,9 @@ import { EuiText, EuiTextArea, htmlIdGenerator, + EuiFlexGroup, + EuiFlexItem, + EuiIconTip, } from '@elastic/eui'; import { cloneDeep } from 'lodash'; import { APP_ID } from '../../../../../../../common/constants'; @@ -252,14 +255,37 @@ export const MalwareProtections = React.memo(() => { {isPlatinumPlus && userNotificationSelected && ( <> - -

- + + +

+ +

+
+
+ + + + + + + } /> -

-
+ + Date: Mon, 14 Dec 2020 13:38:38 -0600 Subject: [PATCH 16/44] [Security Solution] [Sourcerer] Cypress tests (#80410) --- .../cypress/integration/sourcerer.spec.ts | 105 ++++++++++++++ .../cypress/screens/sourcerer.ts | 30 ++++ .../cypress/tasks/sourcerer.ts | 136 ++++++++++++++++++ .../timeline/search_or_filter/pick_events.tsx | 17 ++- 4 files changed, 281 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/integration/sourcerer.spec.ts create mode 100644 x-pack/plugins/security_solution/cypress/screens/sourcerer.ts create mode 100644 x-pack/plugins/security_solution/cypress/tasks/sourcerer.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/sourcerer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/sourcerer.spec.ts new file mode 100644 index 0000000000000..4126bcfdbf0b4 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/sourcerer.spec.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { loginAndWaitForPage } from '../tasks/login'; + +import { HOSTS_URL } from '../urls/navigation'; +import { waitForAllHostsToBeLoaded } from '../tasks/hosts/all_hosts'; +import { + clickOutOfSourcererTimeline, + clickTimelineRadio, + deselectSourcererOptions, + isCustomRadio, + isHostsStatValue, + isNotCustomRadio, + isNotSourcererSelection, + isSourcererOptions, + isSourcererSelection, + openSourcerer, + resetSourcerer, + setSourcererOption, + unsetSourcererOption, +} from '../tasks/sourcerer'; +import { openTimelineUsingToggle } from '../tasks/security_main'; +import { populateTimeline } from '../tasks/timeline'; +import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; + +describe('Sourcerer', () => { + beforeEach(() => { + loginAndWaitForPage(HOSTS_URL); + }); + describe('Default scope', () => { + it('has SIEM index patterns selected on initial load', () => { + openSourcerer(); + isSourcererSelection(`auditbeat-*`); + }); + + it('has Kibana index patterns in the options', () => { + openSourcerer(); + isSourcererOptions([`metrics-*`, `logs-*`]); + }); + it('selected KIP gets added to sourcerer', () => { + setSourcererOption(`metrics-*`); + openSourcerer(); + isSourcererSelection(`metrics-*`); + }); + + it('does not return data without correct pattern selected', () => { + waitForAllHostsToBeLoaded(); + isHostsStatValue('4 '); + setSourcererOption(`metrics-*`); + unsetSourcererOption(`auditbeat-*`); + isHostsStatValue('0 '); + }); + + it('reset button restores to original state', () => { + setSourcererOption(`metrics-*`); + openSourcerer(); + isSourcererSelection(`metrics-*`); + resetSourcerer(); + openSourcerer(); + isNotSourcererSelection(`metrics-*`); + }); + }); + describe('Timeline scope', () => { + const alertPatterns = ['.siem-signals-default']; + const rawPatterns = ['auditbeat-*']; + const allPatterns = [...alertPatterns, ...rawPatterns]; + it('Radio buttons select correct sourcerer patterns', () => { + openTimelineUsingToggle(); + openSourcerer('timeline'); + allPatterns.forEach((ss) => isSourcererSelection(ss, 'timeline')); + clickTimelineRadio('raw'); + rawPatterns.forEach((ss) => isSourcererSelection(ss, 'timeline')); + alertPatterns.forEach((ss) => isNotSourcererSelection(ss, 'timeline')); + clickTimelineRadio('alert'); + alertPatterns.forEach((ss) => isSourcererSelection(ss, 'timeline')); + rawPatterns.forEach((ss) => isNotSourcererSelection(ss, 'timeline')); + }); + it('Adding an option results in the custom radio becoming active', () => { + openTimelineUsingToggle(); + openSourcerer('timeline'); + isNotCustomRadio(); + clickOutOfSourcererTimeline(); + const luckyOption = 'logs-*'; + setSourcererOption(luckyOption, 'timeline'); + openSourcerer('timeline'); + isCustomRadio(); + }); + it('Selected index patterns are properly queried', () => { + openTimelineUsingToggle(); + populateTimeline(); + openSourcerer('timeline'); + deselectSourcererOptions(rawPatterns, 'timeline'); + cy.get(SERVER_SIDE_EVENT_COUNT) + .invoke('text') + .then((strCount) => { + const intCount = +strCount; + cy.wrap(intCount).should('eq', 0); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/screens/sourcerer.ts b/x-pack/plugins/security_solution/cypress/screens/sourcerer.ts new file mode 100644 index 0000000000000..3f461c425c54d --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/screens/sourcerer.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const SOURCERER_TRIGGER = '[data-test-subj="sourcerer-trigger"]'; +export const SOURCERER_INPUT = + '[data-test-subj="indexPattern-switcher"] [data-test-subj="comboBoxInput"]'; +export const SOURCERER_OPTIONS = + '[data-test-subj="comboBoxOptionsList indexPattern-switcher-optionsList"]'; +export const SOURCERER_SAVE_BUTTON = 'button[data-test-subj="add-index"]'; +export const SOURCERER_RESET_BUTTON = 'button[data-test-subj="sourcerer-reset"]'; +export const SOURCERER_POPOVER_TITLE = '.euiPopoverTitle'; +export const HOSTS_STAT = '[data-test-subj="stat-hosts"] [data-test-subj="stat-title"]'; + +export const SOURCERER_TIMELINE = { + trigger: '[data-test-subj="sourcerer-timeline-trigger"]', + advancedSettings: '[data-test-subj="advanced-settings"]', + sourcerer: '[data-test-subj="timeline-sourcerer"]', + sourcererInput: '[data-test-subj="timeline-sourcerer"] [data-test-subj="comboBoxInput"]', + sourcererOptions: '[data-test-subj="comboBoxOptionsList timeline-sourcerer-optionsList"]', + radioRaw: '[data-test-subj="timeline-sourcerer-radio"] label.euiRadio__label[for="raw"]', + radioAlert: '[data-test-subj="timeline-sourcerer-radio"] label.euiRadio__label[for="alert"]', + radioAll: '[data-test-subj="timeline-sourcerer-radio"] label.euiRadio__label[for="all"]', + radioCustom: '[data-test-subj="timeline-sourcerer-radio"] input.euiRadio__input[id="custom"]', + radioCustomLabel: + '[data-test-subj="timeline-sourcerer-radio"] label.euiRadio__label[for="custom"]', +}; +export const SOURCERER_TIMELINE_ADVANCED = '[data-test-subj="advanced-settings"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/sourcerer.ts b/x-pack/plugins/security_solution/cypress/tasks/sourcerer.ts new file mode 100644 index 0000000000000..b224f81ab8f2f --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/tasks/sourcerer.ts @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + HOSTS_STAT, + SOURCERER_INPUT, + SOURCERER_OPTIONS, + SOURCERER_POPOVER_TITLE, + SOURCERER_RESET_BUTTON, + SOURCERER_SAVE_BUTTON, + SOURCERER_TIMELINE, + SOURCERER_TRIGGER, +} from '../screens/sourcerer'; +import { TIMELINE_TITLE } from '../screens/timeline'; + +export const openSourcerer = (sourcererScope?: string) => { + if (sourcererScope != null && sourcererScope === 'timeline') { + return openTimelineSourcerer(); + } + cy.get(SOURCERER_TRIGGER).should('be.enabled'); + cy.get(SOURCERER_TRIGGER).should('be.visible'); + cy.get(SOURCERER_TRIGGER).click(); +}; +export const openTimelineSourcerer = () => { + cy.get(SOURCERER_TIMELINE.trigger).should('be.enabled'); + cy.get(SOURCERER_TIMELINE.trigger).should('be.visible'); + cy.get(SOURCERER_TIMELINE.trigger).click(); + cy.get(SOURCERER_TIMELINE.advancedSettings).should(($div) => { + if ($div.text() === 'Show Advanced') { + $div.click(); + } + expect(true).to.eq(true); + }); +}; +export const openAdvancedSettings = () => {}; + +export const clickOutOfSelector = () => { + return cy.get(SOURCERER_POPOVER_TITLE).first().click(); +}; + +const getScopedSelectors = (sourcererScope?: string): { input: string; options: string } => + sourcererScope != null && sourcererScope === 'timeline' + ? { input: SOURCERER_TIMELINE.sourcererInput, options: SOURCERER_TIMELINE.sourcererOptions } + : { input: SOURCERER_INPUT, options: SOURCERER_OPTIONS }; + +export const isSourcererSelection = (patternName: string, sourcererScope?: string) => { + const { input } = getScopedSelectors(sourcererScope); + return cy.get(input).find(`span[title="${patternName}"]`).should('exist'); +}; + +export const isHostsStatValue = (value: string) => { + return cy.get(HOSTS_STAT).first().should('have.text', value); +}; + +export const isNotSourcererSelection = (patternName: string, sourcererScope?: string) => { + const { input } = getScopedSelectors(sourcererScope); + return cy.get(input).find(`span[title="${patternName}"]`).should('not.exist'); +}; + +export const isSourcererOptions = (patternNames: string[], sourcererScope?: string) => { + const { input, options } = getScopedSelectors(sourcererScope); + cy.get(input).click(); + return patternNames.every((patternName) => { + return cy + .get(options) + .find(`button.euiFilterSelectItem[title="${patternName}"]`) + .its('length') + .should('eq', 1); + }); +}; + +export const selectSourcererOption = (patternName: string, sourcererScope?: string) => { + const { input, options } = getScopedSelectors(sourcererScope); + cy.get(input).click(); + cy.get(options).find(`button.euiFilterSelectItem[title="${patternName}"]`).click(); + clickOutOfSelector(); + return cy.get(SOURCERER_SAVE_BUTTON).click({ force: true }); +}; + +export const deselectSourcererOption = (patternName: string, sourcererScope?: string) => { + const { input } = getScopedSelectors(sourcererScope); + cy.get(input).find(`span[title="${patternName}"] button`).click(); + clickOutOfSelector(); + return cy.get(SOURCERER_SAVE_BUTTON).click({ force: true }); +}; + +export const deselectSourcererOptions = (patternNames: string[], sourcererScope?: string) => { + const { input } = getScopedSelectors(sourcererScope); + patternNames.forEach((patternName) => + cy.get(input).find(`span[title="${patternName}"] button`).click() + ); + clickOutOfSelector(); + return cy.get(SOURCERER_SAVE_BUTTON).click({ force: true }); +}; + +export const resetSourcerer = () => { + cy.get(SOURCERER_RESET_BUTTON).click(); + clickOutOfSelector(); + return cy.get(SOURCERER_SAVE_BUTTON).click({ force: true }); +}; + +export const setSourcererOption = (patternName: string, sourcererScope?: string) => { + openSourcerer(sourcererScope); + isNotSourcererSelection(patternName, sourcererScope); + selectSourcererOption(patternName, sourcererScope); +}; + +export const unsetSourcererOption = (patternName: string, sourcererScope?: string) => { + openSourcerer(sourcererScope); + isSourcererSelection(patternName, sourcererScope); + deselectSourcererOption(patternName, sourcererScope); +}; + +export const clickTimelineRadio = (radioName: string) => { + let theRadio = SOURCERER_TIMELINE.radioAll; + if (radioName === 'alert') { + theRadio = SOURCERER_TIMELINE.radioAlert; + } + if (radioName === 'raw') { + theRadio = SOURCERER_TIMELINE.radioRaw; + } + return cy.get(theRadio).first().click(); +}; + +export const isCustomRadio = () => { + return cy.get(SOURCERER_TIMELINE.radioCustom).should('be.enabled'); +}; + +export const isNotCustomRadio = () => { + return cy.get(SOURCERER_TIMELINE.radioCustom).should('be.disabled'); +}; + +export const clickOutOfSourcererTimeline = () => cy.get(TIMELINE_TITLE).first().click(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx index d7d8d810f6972..3bc0eeeef70a9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx @@ -251,12 +251,13 @@ const PickEventTypeComponents: React.FC = ({ const comboBox = useMemo( () => ( ), [onChangeCombo, indexesPatternOptions, renderOption, selectedOptions] @@ -269,6 +270,7 @@ const PickEventTypeComponents: React.FC = ({ const filter = useMemo( () => ( = ({ const options = getEventTypeOptions(); return ( = ({ const ButtonContent = useMemo( () => ( - + {showAdvanceSettings ? i18n.HIDE_INDEX_PATTERNS_ADVANCED_SETTINGS : i18n.SHOW_INDEX_PATTERNS_ADVANCED_SETTINGS} @@ -330,11 +333,11 @@ const PickEventTypeComponents: React.FC = ({ From ac3e02aeadeda91ac81a0bf313acc4b3bed2fb1e Mon Sep 17 00:00:00 2001 From: Jane Miller <57721870+jmiller263@users.noreply.github.com> Date: Mon, 14 Dec 2020 15:00:22 -0500 Subject: [PATCH 17/44] [SECURITY_SOLUTION] Advanced policy docs (#85203) * started docs, removed kernel harden option * Advanced policy field documentation * consistent formatting * consistent formatting * drop unused fields * grammar * i18n first key * i18n, change versions to 7.9, add some new fields, remove some that we don't want to expose * Update x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts Co-authored-by: Daniel Ferullo <56368752+ferullo@users.noreply.github.com> * Update x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts Co-authored-by: Daniel Ferullo <56368752+ferullo@users.noreply.github.com> * Update x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts Co-authored-by: Daniel Ferullo <56368752+ferullo@users.noreply.github.com> Co-authored-by: Daniel Ferullo <56368752+ferullo@users.noreply.github.com> --- .../policy/models/advanced_policy_schema.ts | 578 +++++++++++++----- 1 file changed, 419 insertions(+), 159 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts index d25588dabedc6..176f64c8bdcb0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; + interface AdvancedPolicySchemaType { key: string; first_supported_version: string; @@ -14,302 +16,560 @@ interface AdvancedPolicySchemaType { export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ { key: 'linux.advanced.agent.connection_delay', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.agent.connection_delay', + { + defaultMessage: + 'How long to wait for agent connectivity before sending first policy reply, in seconds. Default: 60.', + } + ), }, { key: 'linux.advanced.artifacts.global.base_url', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.artifacts.global.base_url', + { + defaultMessage: + 'Base URL from which to download global artifact manifests. Default: https://artifacts.security.elastic.co.', + } + ), }, { key: 'linux.advanced.artifacts.global.manifest_relative_url', - first_supported_version: '7.11', - documentation: '', - }, - { - key: 'linux.advanced.artifacts.global.ca_cert', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.artifacts.global.manifest_relative_url', + { + defaultMessage: + 'Relative URL from which to download global artifact manifests. Default: /downloads/endpoint/manifest/artifacts-.zip.', + } + ), }, { key: 'linux.advanced.artifacts.global.public_key', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.artifacts.global.public_key', + { + defaultMessage: + 'PEM-encoded public key used to verify the global artifact manifest signature.', + } + ), }, { key: 'linux.advanced.artifacts.global.interval', - first_supported_version: '7.11', - documentation: '', - }, - { - key: 'linux.advanced.artifacts.user.base_url', - first_supported_version: '7.11', - documentation: '', - }, - { - key: 'linux.advanced.artifacts.user.ca_cert', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.artifacts.global.interval', + { + defaultMessage: + 'Interval between global artifact manifest download attempts, in seconds. Default: 3600.', + } + ), }, { key: 'linux.advanced.artifacts.user.public_key', - first_supported_version: '7.11', - documentation: '', - }, - { - key: 'linux.advanced.artifacts.user.interval', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.artifacts.user.public_key', + { + defaultMessage: + 'PEM-encoded public key used to verify the user artifact manifest signature.', + } + ), }, { key: 'linux.advanced.elasticsearch.delay', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.elasticsearch.delay', + { + defaultMessage: 'Delay for sending events to Elasticsearch, in seconds. Default: 120.', + } + ), }, { key: 'linux.advanced.elasticsearch.tls.verify_peer', - first_supported_version: '7.11', - documentation: 'default is true', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.elasticsearch.tls.verify_peer', + { + defaultMessage: 'Whether to verify the certificates presented by the peer. Default: true.', + } + ), }, { key: 'linux.advanced.elasticsearch.tls.verify_hostname', - first_supported_version: '7.11', - documentation: 'default is true', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.elasticsearch.tls.verify_hostname', + { + defaultMessage: + "Whether to verify the hostname of the peer is what's in the certificate. Default: true.", + } + ), }, { key: 'linux.advanced.elasticsearch.tls.ca_cert', - first_supported_version: '7.11', - documentation: '', - }, - { - key: 'mac.advanced.agent.connection_delay', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.elasticsearch.tls.ca_cert', + { + defaultMessage: 'PEM-encoded certificate for Elasticsearch certificate authority.', + } + ), }, { - key: 'mac.advanced.artifacts.global.base_url', + key: 'linux.advanced.logging.file', first_supported_version: '7.11', - documentation: '', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.logging.file', + { + defaultMessage: + 'A supplied value will override the log level configured for logs that are saved to disk and streamed to Elasticsearch. It is recommended Fleet be used to change this logging in most circumstances. Allowed values are error, warning, info, debug, and trace.', + } + ), }, { - key: 'mac.advanced.artifacts.global.manifest_relative_url', + key: 'linux.advanced.logging.syslog', first_supported_version: '7.11', - documentation: '', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.logging.syslog', + { + defaultMessage: + 'A supplied value will configure logging to syslog. Allowed values are error, warning, info, debug, and trace.', + } + ), }, { - key: 'mac.advanced.artifacts.global.ca_cert', - first_supported_version: '7.11', - documentation: '', + key: 'mac.advanced.agent.connection_delay', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.agent.connection_delay', + { + defaultMessage: + 'How long to wait for agent connectivity before sending first policy reply, in seconds. Default: 60.', + } + ), }, { - key: 'mac.advanced.artifacts.global.public_key', - first_supported_version: '7.11', - documentation: '', + key: 'mac.advanced.artifacts.global.base_url', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.artifacts.global.base_url', + { + defaultMessage: 'URL from which to download global artifact manifests.', + } + ), }, { - key: 'mac.advanced.artifacts.global.interval', - first_supported_version: '7.11', - documentation: '', + key: 'mac.advanced.artifacts.global.manifest_relative_url', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.artifacts.global.manifest_relative_url', + { + defaultMessage: + 'Relative URL from which to download global artifact manifests. Default: /downloads/endpoint/manifest/artifacts-.zip.', + } + ), }, { - key: 'mac.advanced.artifacts.user.base_url', - first_supported_version: '7.11', - documentation: '', + key: 'mac.advanced.artifacts.global.public_key', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.artifacts.global.public_key', + { + defaultMessage: + 'PEM-encoded public key used to verify the global artifact manifest signature.', + } + ), }, { - key: 'mac.advanced.artifacts.user.ca_cert', - first_supported_version: '7.11', - documentation: '', + key: 'mac.advanced.artifacts.global.interval', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.artifacts.global.interval', + { + defaultMessage: + 'Interval between global artifact manifest download attempts, in seconds. Default: 3600.', + } + ), }, { key: 'mac.advanced.artifacts.user.public_key', - first_supported_version: '7.11', - documentation: '', - }, - { - key: 'mac.advanced.artifacts.user.interval', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.artifacts.user.public_key', + { + defaultMessage: + 'PEM-encoded public key used to verify the user artifact manifest signature.', + } + ), }, { key: 'mac.advanced.elasticsearch.delay', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.elasticsearch.delay', + { + defaultMessage: 'Delay for sending events to Elasticsearch, in seconds. Default: 120.', + } + ), }, { key: 'mac.advanced.elasticsearch.tls.verify_peer', - first_supported_version: '7.11', - documentation: 'default is true', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.elasticsearch.tls.verify_peer', + { + defaultMessage: 'Whether to verify the certificates presented by the peer. Default: true.', + } + ), }, { key: 'mac.advanced.elasticsearch.tls.verify_hostname', - first_supported_version: '7.11', - documentation: 'default is true', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.elasticsearch.tls.verify_hostname', + { + defaultMessage: + "Whether to verify the hostname of the peer is what's in the certificate. Default: true.", + } + ), }, { key: 'mac.advanced.elasticsearch.tls.ca_cert', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.elasticsearch.tls.ca_cert', + { + defaultMessage: 'PEM-encoded certificate for Elasticsearch certificate authority.', + } + ), }, { - key: 'mac.advanced.malware.quarantine', + key: 'mac.advanced.logging.file', first_supported_version: '7.11', - documentation: '', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.logging.file', + { + defaultMessage: + 'A supplied value will override the log level configured for logs that are saved to disk and streamed to Elasticsearch. It is recommended Fleet be used to change this logging in most circumstances. Allowed values are error, warning, info, debug, and trace.', + } + ), }, { - key: 'mac.advanced.kernel.connect', + key: 'mac.advanced.logging.syslog', first_supported_version: '7.11', - documentation: '', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.logging.syslog', + { + defaultMessage: + 'A supplied value will configure logging to syslog. Allowed values are error, warning, info, debug, and trace.', + } + ), }, { - key: 'mac.advanced.kernel.harden', - first_supported_version: '7.11', - documentation: '', + key: 'mac.advanced.malware.quarantine', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.malware.quarantine', + { + defaultMessage: + 'Whether quarantine should be enabled when malware prevention is enabled. Default: true.', + } + ), + }, + { + key: 'mac.advanced.kernel.connect', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.kernel.connect', + { + defaultMessage: 'Whether to connect to the kernel driver. Default: true.', + } + ), }, { key: 'mac.advanced.kernel.process', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.kernel.process', + { + defaultMessage: + "A value of 'false' overrides other config settings that would enable kernel process events. Default: true.", + } + ), }, { key: 'mac.advanced.kernel.filewrite', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.kernel.filewrite', + { + defaultMessage: + "A value of 'false' overrides other config settings that would enable kernel file write events. Default: true.", + } + ), }, { key: 'mac.advanced.kernel.network', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.kernel.network', + { + defaultMessage: + "A value of 'false' overrides other config settings that would enable kernel network events. Default: true.", + } + ), + }, + { + key: 'mac.advanced.harden.self_protect', first_supported_version: '7.11', - documentation: '', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.harden.self_protect', + { + defaultMessage: 'Enables self-protection on macOS. Default: true.', + } + ), }, { key: 'windows.advanced.agent.connection_delay', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.agent.connection_delay', + { + defaultMessage: + 'How long to wait for agent connectivity before sending first policy reply, in seconds. Default: 60.', + } + ), }, { key: 'windows.advanced.artifacts.global.base_url', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.artifacts.global.base_url', + { + defaultMessage: 'URL from which to download global artifact manifests.', + } + ), }, { key: 'windows.advanced.artifacts.global.manifest_relative_url', - first_supported_version: '7.11', - documentation: '', - }, - { - key: 'windows.advanced.artifacts.global.ca_cert', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.artifacts.global.manifest_relative_url', + { + defaultMessage: + 'Relative URL from which to download global artifact manifests. Default: /downloads/endpoint/manifest/artifacts-.zip.', + } + ), }, { key: 'windows.advanced.artifacts.global.public_key', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.artifacts.global.public_key', + { + defaultMessage: + 'PEM-encoded public key used to verify the global artifact manifest signature.', + } + ), }, { key: 'windows.advanced.artifacts.global.interval', - first_supported_version: '7.11', - documentation: '', - }, - { - key: 'windows.advanced.artifacts.user.base_url', - first_supported_version: '7.11', - documentation: '', - }, - { - key: 'windows.advanced.artifacts.user.ca_cert', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.artifacts.global.interval', + { + defaultMessage: + 'Interval between global artifact manifest download attempts, in seconds. Default: 3600.', + } + ), }, { key: 'windows.advanced.artifacts.user.public_key', - first_supported_version: '7.11', - documentation: '', - }, - { - key: 'windows.advanced.artifacts.user.interval', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.artifacts.user.public_key', + { + defaultMessage: + 'PEM-encoded public key used to verify the user artifact manifest signature.', + } + ), }, { key: 'windows.advanced.elasticsearch.delay', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.elasticsearch.delay', + { + defaultMessage: 'Delay for sending events to Elasticsearch, in seconds. Default: 120.', + } + ), }, { key: 'windows.advanced.elasticsearch.tls.verify_peer', - first_supported_version: '7.11', - documentation: 'default is true', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.elasticsearch.tls.verify_peer', + { + defaultMessage: 'Whether to verify the certificates presented by the peer. Default: true.', + } + ), }, { key: 'windows.advanced.elasticsearch.tls.verify_hostname', - first_supported_version: '7.11', - documentation: 'default is true', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.elasticsearch.tls.verify_hostname', + { + defaultMessage: + "Whether to verify the hostname of the peer is what's in the certificate. Default: true.", + } + ), }, { key: 'windows.advanced.elasticsearch.tls.ca_cert', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.elasticsearch.tls.ca_cert', + { + defaultMessage: 'PEM-encoded certificate for Elasticsearch certificate authority.', + } + ), }, { - key: 'windows.advanced.malware.quarantine', + key: 'windows.advanced.logging.file', first_supported_version: '7.11', - documentation: '', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.logging.file', + { + defaultMessage: + 'A supplied value will override the log level configured for logs that are saved to disk and streamed to Elasticsearch. It is recommended Fleet be used to change this logging in most circumstances. Allowed values are error, warning, info, debug, and trace.', + } + ), }, { - key: 'windows.advanced.ransomware.mbr', + key: 'windows.advanced.logging.debugview', first_supported_version: '7.11', - documentation: '', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.logging.debugview', + { + defaultMessage: + 'A supplied value will configure logging to Debugview (a Sysinternals tool). Allowed values are error, warning, info, debug, and trace.', + } + ), }, { - key: 'windows.advanced.ransomware.canary', - first_supported_version: '7.11', - documentation: '', + key: 'windows.advanced.malware.quarantine', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.malware.quarantine', + { + defaultMessage: + 'Whether quarantine should be enabled when malware prevention is enabled. Default: true.', + } + ), }, { key: 'windows.advanced.kernel.connect', - first_supported_version: '7.11', - documentation: '', - }, - { - key: 'windows.advanced.kernel.harden', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.kernel.connect', + { + defaultMessage: 'Whether to connect to the kernel driver. Default: true.', + } + ), }, { key: 'windows.advanced.kernel.process', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.kernel.process', + { + defaultMessage: + "A value of 'false' overrides other config settings that would enable kernel process events. Default: true.", + } + ), }, { key: 'windows.advanced.kernel.filewrite', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.kernel.filewrite', + { + defaultMessage: + "A value of 'false' overrides other config settings that would enable kernel file write events. Default: true.", + } + ), }, { key: 'windows.advanced.kernel.network', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.kernel.network', + { + defaultMessage: + "A value of 'false' overrides other config settings that would enable kernel network events. Default: true.", + } + ), }, { key: 'windows.advanced.kernel.fileopen', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.kernel.fileopen', + { + defaultMessage: + "A value of 'false' overrides other config settings that would enable kernel file open events. Default: true.", + } + ), }, { key: 'windows.advanced.kernel.asyncimageload', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.kernel.asyncimageload', + { + defaultMessage: + "A value of 'false' overrides other config settings that would enable kernel async image load events. Default: true.", + } + ), }, { key: 'windows.advanced.kernel.syncimageload', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.kernel.syncimageload', + { + defaultMessage: + "A value of 'false' overrides other config settings that would enable kernel sync image load events. Default: true.", + } + ), }, { key: 'windows.advanced.kernel.registry', - first_supported_version: '7.11', - documentation: '', + first_supported_version: '7.9', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.kernel.registry', + { + defaultMessage: + "A value of 'false' overrides other config settings that would enable kernel registry events. Default: true.", + } + ), + }, + { + key: 'windows.advanced.diagnostic.enabled', + first_supported_version: '7.11', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.diagnostic.enabled', + { + defaultMessage: + "A value of 'false' disables running diagnostic features on Endpoint. Default: true.", + } + ), }, ]; From b8ab3fdafc7562b1cfc85b354971a93c7a218f9e Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 14 Dec 2020 21:10:00 +0100 Subject: [PATCH 18/44] [ILM] Rollover field redesign (#85579) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * implement form-level support for using default rollover action * slight update to copy * added use default rollover switch and tooltips for detailed copy * fix legacy integration tests and do not unmount rollover field!! * remove unused import * fix client integration tests * updated form to use new isUsingRollover check * fix serialization of rollover * Update x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx Co-authored-by: Adam Locke * Update x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx Co-authored-by: Adam Locke * Update x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx Co-authored-by: Adam Locke Co-authored-by: Yulia Čech <6585477+yuliacech@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Adam Locke --- .../edit_policy/edit_policy.helpers.tsx | 3 + .../edit_policy/edit_policy.test.ts | 20 +- .../__jest__/components/edit_policy.test.tsx | 12 + .../common/types/policies.ts | 12 +- .../public/application/constants/policy.ts | 17 +- .../public/application/lib/index.ts | 2 + .../public/application/lib/rollover.ts | 18 + .../described_form_row/described_form_row.tsx | 4 +- .../components/phases/hot_phase/hot_phase.tsx | 325 ++++++++++-------- .../components/phases/shared_fields/index.ts | 2 - .../min_age_input_field.tsx | 13 +- .../searchable_snapshot_field.tsx | 9 +- .../phases/warm_phase/warm_phase.tsx | 10 +- .../sections/edit_policy/constants.ts | 2 + .../form/configuration_issues_context.tsx | 17 +- .../sections/edit_policy/form/deserializer.ts | 3 +- .../sections/edit_policy/form/schema.ts | 6 + .../edit_policy/form/serializer/serializer.ts | 8 +- .../application/sections/edit_policy/types.ts | 1 + 19 files changed, 300 insertions(+), 184 deletions(-) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/lib/rollover.ts 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 abb33d109742c..7206fbfd547d4 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 @@ -111,6 +111,8 @@ export const setup = async (arg?: { appServicesContext: Partial { @@ -239,6 +241,7 @@ export const setup = async (arg?: { appServicesContext: Partial', () => { test('setting all values', async () => { const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); await actions.hot.setMaxSize('123', 'mb'); await actions.hot.setMaxDocs('123'); await actions.hot.setMaxAge('123', 'h'); @@ -177,7 +178,8 @@ describe('', () => { test('disabling rollover', async () => { const { actions } = testBed; - await actions.hot.toggleRollover(true); + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); await actions.savePolicy(); const latestRequest = server.requests[server.requests.length - 1]; const policy = JSON.parse(JSON.parse(latestRequest.requestBody).body); @@ -212,6 +214,17 @@ describe('', () => { expect(actions.cold.searchableSnapshotsExists()).toBeTruthy(); expect(actions.cold.freezeExists()).toBeFalsy(); }); + + test('disabling rollover toggle, but enabling default rollover', async () => { + const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + await actions.hot.toggleDefaultRollover(true); + + expect(actions.hot.forceMergeFieldExists()).toBeTruthy(); + expect(actions.hot.shrinkExists()).toBeTruthy(); + expect(actions.hot.searchableSnapshotsExists()).toBeTruthy(); + }); }); }); @@ -766,7 +779,7 @@ describe('', () => { await act(async () => { testBed = await setup({ appServicesContext: { - license: licensingMock.createLicense({ license: { type: 'basic' } }), + license: licensingMock.createLicense({ license: { type: 'enterprise' } }), }, }); }); @@ -776,11 +789,12 @@ describe('', () => { }); test('hiding and disabling searchable snapshot field', async () => { const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); await actions.hot.toggleRollover(false); await actions.cold.enable(true); expect(actions.hot.searchableSnapshotsExists()).toBeFalsy(); - expect(actions.cold.searchableSnapshotDisabledDueToLicense()).toBeTruthy(); + expect(actions.cold.searchableSnapshotDisabledDueToRollover()).toBeTruthy(); }); }); }); 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 index d7d38e3b92516..c54ccb9f85edf 100644 --- 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 @@ -113,7 +113,14 @@ const expectedErrorMessages = (rendered: ReactWrapper, expectedMessages: string[ 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'); }); @@ -326,6 +333,7 @@ describe('edit policy', () => { 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'); @@ -349,6 +357,7 @@ describe('edit policy', () => { 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' } }); @@ -360,6 +369,7 @@ describe('edit policy', () => { 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' } }); @@ -370,6 +380,7 @@ describe('edit policy', () => { 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' } }); @@ -380,6 +391,7 @@ describe('edit policy', () => { 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' } }); diff --git a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts index 4f7782a51b278..58468f06e3b2d 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -57,13 +57,15 @@ export interface SearchableSnapshotAction { force_merge_index?: boolean; } +export interface RolloverAction { + max_size?: string; + max_age?: string; + max_docs?: number; +} + export interface SerializedHotPhase extends SerializedPhase { actions: { - rollover?: { - max_size?: string; - max_age?: string; - max_docs?: number; - }; + rollover?: RolloverAction; forcemerge?: ForcemergeAction; readonly?: {}; shrink?: ShrinkAction; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts index 23d7387aa7076..a892a7a031a87 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts @@ -4,21 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SerializedPhase, DeletePhase, SerializedPolicy } from '../../../common/types'; +import { + SerializedPhase, + DeletePhase, + SerializedPolicy, + RolloverAction, +} from '../../../common/types'; export const defaultSetPriority: string = '100'; export const defaultPhaseIndexPriority: string = '50'; +export const defaultRolloverAction: RolloverAction = { + max_age: '30d', + max_size: '50gb', +}; + export const defaultPolicy: SerializedPolicy = { name: '', phases: { hot: { actions: { - rollover: { - max_age: '30d', - max_size: '50gb', - }, + rollover: defaultRolloverAction, }, }, }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/index.ts index 1dabae1a0f0c4..274905342f815 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/lib/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/index.ts @@ -5,3 +5,5 @@ */ export * from './data_tiers'; + +export * from './rollover'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/rollover.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/rollover.ts new file mode 100644 index 0000000000000..1b85303c4bce0 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/rollover.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SerializedPolicy } from '../../../common/types'; +import { defaultRolloverAction } from '../constants'; + +export const isUsingDefaultRollover = (policy: SerializedPolicy): boolean => { + const rollover = policy?.phases?.hot?.actions?.rollover; + return Boolean( + rollover && + rollover.max_age === defaultRolloverAction.max_age && + rollover.max_docs === defaultRolloverAction.max_docs && + rollover.max_size === defaultRolloverAction.max_size + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/described_form_row.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/described_form_row.tsx index 98c63437659fd..161729ae48057 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/described_form_row.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/described_form_row/described_form_row.tsx @@ -55,7 +55,9 @@ export const DescribedFormRow: FunctionComponent = ({ const [uncontrolledIsContentVisible, setUncontrolledIsContentVisible] = useState( () => switchProps?.initialValue ?? false ); - const isContentVisible = Boolean(switchProps?.checked ?? uncontrolledIsContentVisible); + const isContentVisible = Boolean( + switchProps === undefined || (switchProps?.checked ?? uncontrolledIsContentVisible) + ); const renderToggle = () => { if (!switchProps) { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx index 77f36a237c0c4..ae8fecd1a1958 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx @@ -16,6 +16,8 @@ import { EuiCallOut, EuiAccordion, EuiTextColor, + EuiSwitch, + EuiIconTip, } from '@elastic/eui'; import { Phases } from '../../../../../../../common/types'; @@ -24,19 +26,18 @@ import { useFormData, UseField, SelectField, NumericField } from '../../../../.. import { i18nTexts } from '../../../i18n_texts'; -import { ROLLOVER_EMPTY_VALIDATION } from '../../../form'; +import { ROLLOVER_EMPTY_VALIDATION, useConfigurationIssues } from '../../../form'; import { useEditPolicyContext } from '../../../edit_policy_context'; -import { ROLLOVER_FORM_PATHS } from '../../../constants'; +import { ROLLOVER_FORM_PATHS, isUsingDefaultRolloverPath } from '../../../constants'; -import { LearnMoreLink, ActiveBadge, ToggleFieldWithDescribedFormRow } from '../../'; +import { LearnMoreLink, ActiveBadge, DescribedFormRow } from '../../'; import { ForcemergeField, SetPriorityInputField, SearchableSnapshotField, - useRolloverPath, ReadonlyField, ShrinkField, } from '../shared_fields'; @@ -48,9 +49,10 @@ const hotProperty: keyof Phases = 'hot'; export const HotPhase: FunctionComponent = () => { const { license } = useEditPolicyContext(); const [formData] = useFormData({ - watch: useRolloverPath, + watch: isUsingDefaultRolloverPath, }); - const isRolloverEnabled = get(formData, useRolloverPath); + const { isUsingRollover } = useConfigurationIssues(); + const isUsingDefaultRollover = get(formData, isUsingDefaultRolloverPath); const [showEmptyRolloverFieldsError, setShowEmptyRolloverFieldsError] = useState(false); return ( @@ -89,7 +91,7 @@ export const HotPhase: FunctionComponent = () => { })} paddingSize="m" > - {i18n.translate('xpack.indexLifecycleMgmt.hotPhase.rolloverFieldTitle', { @@ -98,143 +100,192 @@ export const HotPhase: FunctionComponent = () => { } description={ - -

- {' '} - + +

+ {' '} + + } + docPath="indices-rollover-index.html" + /> +

+
+ + path={isUsingDefaultRolloverPath}> + {(field) => ( + <> + field.setValue(e.target.checked)} + data-test-subj="useDefaultRolloverSwitch" + /> +   + + } /> - } - docPath="indices-rollover-index.html" - /> -

- + + )} + + } - switchProps={{ - path: '_meta.hot.useRollover', - 'data-test-subj': 'rolloverSwitch', - }} fullWidth > - {isRolloverEnabled && ( - <> - - {showEmptyRolloverFieldsError && ( +
+ path="_meta.hot.useRollover"> + {(field) => ( <> - -
{i18nTexts.editPolicy.errors.rollOverConfigurationCallout.body}
-
- - - )} - - - - {(field) => { - const showErrorCallout = field.errors.some( - (e) => e.code === ROLLOVER_EMPTY_VALIDATION - ); - if (showErrorCallout !== showEmptyRolloverFieldsError) { - setShowEmptyRolloverFieldsError(showErrorCallout); - } - return ( - - ); - }} - - - - field.setValue(e.target.checked)} + data-test-subj="rolloverSwitch" /> - - - - - - - - - - - - + } /> - - - - - - - )} - - {isRolloverEnabled && ( + + )} + + {isUsingRollover && ( + <> + + {showEmptyRolloverFieldsError && ( + <> + +
{i18nTexts.editPolicy.errors.rollOverConfigurationCallout.body}
+
+ + + )} + + + + {(field) => { + const showErrorCallout = field.errors.some( + (e) => e.code === ROLLOVER_EMPTY_VALIDATION + ); + if (showErrorCallout !== showEmptyRolloverFieldsError) { + setShowEmptyRolloverFieldsError(showErrorCallout); + } + return ( + + ); + }} + + + + + + + + + + + + + + + + + + + + + + + )} +
+ + {isUsingRollover && ( <> {} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts index e56b0b21491f3..15167672265fd 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { useRolloverPath } from '../../../constants'; - export { DataTierAllocationField } from './data_tier_allocation_field'; export { ForcemergeField } from './forcemerge_field'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_input_field/min_age_input_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_input_field/min_age_input_field.tsx index f37c387354418..59086ce572252 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_input_field/min_age_input_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_input_field/min_age_input_field.tsx @@ -4,21 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { FunctionComponent } from 'react'; -import { get } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { - useFormData, - UseField, - NumericField, - SelectField, -} from '../../../../../../../shared_imports'; +import { UseField, NumericField, SelectField } from '../../../../../../../shared_imports'; import { LearnMoreLink } from '../../../learn_more_link'; -import { useRolloverPath } from '../../../../constants'; +import { useConfigurationIssues } from '../../../../form'; import { getUnitsAriaLabelForPhase, getTimingLabelForPhase } from './util'; @@ -29,8 +23,7 @@ interface Props { } export const MinAgeInputField: FunctionComponent = ({ phase }): React.ReactElement => { - const [formData] = useFormData({ watch: useRolloverPath }); - const rolloverEnabled = get(formData, useRolloverPath); + const { isUsingRollover: rolloverEnabled } = useConfigurationIssues(); let daysOptionLabel; let hoursOptionLabel; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx index 2a55cee0794c5..3157c0a51accf 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx @@ -29,8 +29,6 @@ import { useConfigurationIssues } from '../../../../form'; import { i18nTexts } from '../../../../i18n_texts'; -import { useRolloverPath } from '../../../../constants'; - import { FieldLoadingError, DescribedFormRow, LearnMoreLink } from '../../../'; import { SearchableSnapshotDataProvider } from './searchable_snapshot_data_provider'; @@ -54,17 +52,16 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) => services: { cloud }, } = useKibana(); const { getUrlForApp, policy, license } = useEditPolicyContext(); - const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); + const { isUsingSearchableSnapshotInHotPhase, isUsingRollover } = useConfigurationIssues(); const searchableSnapshotPath = `phases.${phase}.actions.searchable_snapshot.snapshot_repository`; - const [formData] = useFormData({ watch: [searchableSnapshotPath, useRolloverPath] }); - const isRolloverEnabled = get(formData, useRolloverPath); + const [formData] = useFormData({ watch: searchableSnapshotPath }); const searchableSnapshotRepo = get(formData, searchableSnapshotPath); const isDisabledDueToLicense = !license.canUseSearchableSnapshot(); const isDisabledInColdDueToHotPhase = phase === 'cold' && isUsingSearchableSnapshotInHotPhase; - const isDisabledInColdDueToRollover = phase === 'cold' && !isRolloverEnabled; + const isDisabledInColdDueToRollover = phase === 'cold' && !isUsingRollover; const isDisabled = isDisabledDueToLicense || isDisabledInColdDueToHotPhase || isDisabledInColdDueToRollover; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx index 36a39eb7c110f..77078e94d7e98 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx @@ -21,7 +21,6 @@ import { useConfigurationIssues } from '../../../form'; import { ActiveBadge, DescribedFormRow } from '../../'; import { - useRolloverPath, MinAgeInputField, ForcemergeField, SetPriorityInputField, @@ -47,13 +46,12 @@ const formFieldPaths = { export const WarmPhase: FunctionComponent = () => { const { policy } = useEditPolicyContext(); - const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); + const { isUsingSearchableSnapshotInHotPhase, isUsingRollover } = useConfigurationIssues(); const [formData] = useFormData({ - watch: [useRolloverPath, formFieldPaths.enabled, formFieldPaths.warmPhaseOnRollover], + watch: [formFieldPaths.enabled, formFieldPaths.warmPhaseOnRollover], }); const enabled = get(formData, formFieldPaths.enabled); - const hotPhaseRolloverEnabled = get(formData, useRolloverPath); const warmPhaseOnRollover = get(formData, formFieldPaths.warmPhaseOnRollover); return ( @@ -99,7 +97,7 @@ export const WarmPhase: FunctionComponent = () => { <> {enabled && ( <> - {hotPhaseRolloverEnabled && ( + {isUsingRollover && ( { }} /> )} - {(!warmPhaseOnRollover || !hotPhaseRolloverEnabled) && ( + {(!warmPhaseOnRollover || !isUsingRollover) && ( <> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts index a5d5f1c62847c..48ed38fc8a0d7 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/constants.ts @@ -6,6 +6,8 @@ export const useRolloverPath = '_meta.hot.useRollover'; +export const isUsingDefaultRolloverPath = '_meta.hot.isUsingDefaultRollover'; + /** * These strings describe the path to their respective values in the serialized * ILM form. diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx index c31eb5bdaa329..3a66abebccc1a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx @@ -6,10 +6,17 @@ import { get } from 'lodash'; import React, { FunctionComponent, createContext, useContext } from 'react'; + import { useFormData } from '../../../../shared_imports'; +import { isUsingDefaultRolloverPath, useRolloverPath } from '../constants'; + export interface ConfigurationIssues { - isUsingForceMergeInHotPhase: boolean; + /** + * Whether the serialized policy will use rollover. This blocks certain actions in + * the form such as hot phase (forcemerge, shrink) and cold phase (searchable snapshot). + */ + isUsingRollover: boolean; /** * If this value is true, phases after hot cannot set shrink, forcemerge, freeze, or * searchable_snapshot actions. @@ -24,18 +31,18 @@ const ConfigurationIssuesContext = createContext(null as an const pathToHotPhaseSearchableSnapshot = 'phases.hot.actions.searchable_snapshot.snapshot_repository'; -const pathToHotForceMerge = 'phases.hot.actions.forcemerge.max_num_segments'; - export const ConfigurationIssuesProvider: FunctionComponent = ({ children }) => { const [formData] = useFormData({ - watch: [pathToHotPhaseSearchableSnapshot, pathToHotForceMerge], + watch: [pathToHotPhaseSearchableSnapshot, useRolloverPath, isUsingDefaultRolloverPath], }); + const isUsingDefaultRollover = get(formData, isUsingDefaultRolloverPath); + const rolloverSwitchEnabled = get(formData, useRolloverPath); return ( {children} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts index 0a85a376a9ab4..160c3987f8898 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts @@ -10,7 +10,7 @@ import { SerializedPolicy } from '../../../../../common/types'; import { splitSizeAndUnits } from '../../../lib/policies'; -import { determineDataTierAllocationType } from '../../../lib'; +import { determineDataTierAllocationType, isUsingDefaultRollover } from '../../../lib'; import { FormInternal } from '../types'; @@ -22,6 +22,7 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => { const _meta: FormInternal['_meta'] = { hot: { useRollover: Boolean(hot?.actions?.rollover), + isUsingDefaultRollover: isUsingDefaultRollover(policy), bestCompression: hot?.actions?.forcemerge?.index_codec === 'best_compression', readonlyEnabled: Boolean(hot?.actions?.readonly), }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index a292a888e78c4..ae2432971059c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -38,6 +38,12 @@ export const schema: FormSchema = { defaultMessage: 'Enable rollover', }), }, + isUsingDefaultRollover: { + defaultValue: true, + label: i18n.translate('xpack.indexLifecycleMgmt.hotPhase.isUsingDefaultRollover', { + defaultMessage: 'Use recommended defaults', + }), + }, maxStorageSizeUnit: { defaultValue: 'gb', }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts index 75935f149534e..2a7689b42554e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -6,11 +6,11 @@ import { produce } from 'immer'; -import { merge } from 'lodash'; +import { merge, cloneDeep } from 'lodash'; import { SerializedPolicy } from '../../../../../../common/types'; -import { defaultPolicy } from '../../../../constants'; +import { defaultPolicy, defaultRolloverAction } from '../../../../constants'; import { FormInternal } from '../../types'; @@ -42,7 +42,9 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( if (draft.phases.hot?.actions) { const hotPhaseActions = draft.phases.hot.actions; - if (hotPhaseActions.rollover && _meta.hot.useRollover) { + if (_meta.hot.isUsingDefaultRollover) { + hotPhaseActions.rollover = cloneDeep(defaultRolloverAction); + } else if (hotPhaseActions.rollover && _meta.hot.useRollover) { if (updatedPolicy.phases.hot!.actions.rollover?.max_age) { hotPhaseActions.rollover.max_age = `${hotPhaseActions.rollover.max_age}${_meta.hot.maxAgeUnit}`; } else { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts index f04acea0bbf0a..4dfd7503b9973 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts @@ -23,6 +23,7 @@ export interface ForcemergeFields { interface HotPhaseMetaFields extends ForcemergeFields { useRollover: boolean; + isUsingDefaultRollover: boolean; maxStorageSizeUnit?: string; maxAgeUnit?: string; readonlyEnabled: boolean; From dd7bbb817b168e7033f419c6a67c1a980175a152 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Mon, 14 Dec 2020 20:59:12 +0000 Subject: [PATCH 19/44] [Synthetics] Waterfall view (#84821) * Add a new synthetics step detail page for displaying waterfall data --- x-pack/plugins/uptime/common/constants/ui.ts | 2 + .../uptime/common/runtime_types/index.ts | 1 + .../common/runtime_types/network_events.ts | 48 ++ .../uptime/common/runtime_types/ping/ping.ts | 30 +- .../components/common/header/page_header.tsx | 10 +- .../components/common/step_detail_link.tsx | 32 + .../__tests__/executed_journey.test.tsx | 2 + .../__tests__/executed_step.test.tsx | 352 ++++++++- .../monitor/synthetics/executed_journey.tsx | 2 +- .../monitor/synthetics/executed_step.tsx | 160 ++-- .../synthetics/step_detail/step_detail.tsx | 142 ++++ .../step_detail/step_detail_container.tsx | 114 +++ .../waterfall/data_formatting.test.ts | 27 + .../step_detail/waterfall/data_formatting.ts | 209 ++++++ .../waterfall}/types.ts | 19 +- .../waterfall/waterfall_chart_container.tsx | 66 ++ .../waterfall}/waterfall_chart_wrapper.tsx | 13 +- .../waterfall/components/constants.ts | 3 + .../waterfall/components/sidebar.tsx | 6 +- .../synthetics/waterfall/components/styles.ts | 5 +- .../waterfall/components/waterfall_chart.tsx | 27 +- .../synthetics/data_formatting.test.ts | 687 ------------------ .../consumers/synthetics/data_formatting.ts | 336 --------- .../uptime/public/hooks/use_telemetry.ts | 1 + x-pack/plugins/uptime/public/pages/index.ts | 1 + .../uptime/public/pages/step_detail_page.tsx | 20 + x-pack/plugins/uptime/public/routes.tsx | 10 +- .../public/state/actions/network_events.ts | 27 + .../uptime/public/state/api/network_events.ts | 25 + .../uptime/public/state/effects/index.ts | 2 + .../public/state/effects/network_events.ts | 39 + .../uptime/public/state/reducers/index.ts | 2 + .../uptime/public/state/reducers/journey.ts | 4 +- .../public/state/reducers/network_events.ts | 122 ++++ .../state/selectors/__tests__/index.test.ts | 1 + .../uptime/public/state/selectors/index.ts | 2 + .../__tests__/get_network_events.test.ts | 251 +++++++ .../lib/requests/get_journey_details.ts | 127 ++++ .../server/lib/requests/get_network_events.ts | 59 ++ .../uptime/server/lib/requests/index.ts | 4 + .../plugins/uptime/server/rest_api/index.ts | 2 + .../network_events/get_network_events.ts | 33 + .../server/rest_api/network_events/index.ts | 7 + .../uptime/server/rest_api/pings/journeys.ts | 6 + 44 files changed, 1892 insertions(+), 1146 deletions(-) create mode 100644 x-pack/plugins/uptime/common/runtime_types/network_events.ts create mode 100644 x-pack/plugins/uptime/public/components/common/step_detail_link.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts rename x-pack/plugins/uptime/public/components/monitor/synthetics/{waterfall/consumers/synthetics => step_detail/waterfall}/types.ts (86%) create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx rename x-pack/plugins/uptime/public/components/monitor/synthetics/{waterfall/consumers/synthetics => step_detail/waterfall}/waterfall_chart_wrapper.tsx (91%) delete mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.test.ts delete mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.ts create mode 100644 x-pack/plugins/uptime/public/pages/step_detail_page.tsx create mode 100644 x-pack/plugins/uptime/public/state/actions/network_events.ts create mode 100644 x-pack/plugins/uptime/public/state/api/network_events.ts create mode 100644 x-pack/plugins/uptime/public/state/effects/network_events.ts create mode 100644 x-pack/plugins/uptime/public/state/reducers/network_events.ts create mode 100644 x-pack/plugins/uptime/server/lib/requests/__tests__/get_network_events.test.ts create mode 100644 x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts create mode 100644 x-pack/plugins/uptime/server/lib/requests/get_network_events.ts create mode 100644 x-pack/plugins/uptime/server/rest_api/network_events/get_network_events.ts create mode 100644 x-pack/plugins/uptime/server/rest_api/network_events/index.ts diff --git a/x-pack/plugins/uptime/common/constants/ui.ts b/x-pack/plugins/uptime/common/constants/ui.ts index 2fc7c33e71630..24482004ba4e8 100644 --- a/x-pack/plugins/uptime/common/constants/ui.ts +++ b/x-pack/plugins/uptime/common/constants/ui.ts @@ -12,6 +12,8 @@ export const SETTINGS_ROUTE = '/settings'; export const CERTIFICATES_ROUTE = '/certificates'; +export const STEP_DETAIL_ROUTE = '/journey/:checkGroupId/step/:stepIndex'; + export enum STATUS { UP = 'up', DOWN = 'down', diff --git a/x-pack/plugins/uptime/common/runtime_types/index.ts b/x-pack/plugins/uptime/common/runtime_types/index.ts index e80471bf8b56f..43487eca69e9b 100644 --- a/x-pack/plugins/uptime/common/runtime_types/index.ts +++ b/x-pack/plugins/uptime/common/runtime_types/index.ts @@ -12,3 +12,4 @@ export * from './monitor'; export * from './overview_filters'; export * from './ping'; export * from './snapshot'; +export * from './network_events'; diff --git a/x-pack/plugins/uptime/common/runtime_types/network_events.ts b/x-pack/plugins/uptime/common/runtime_types/network_events.ts new file mode 100644 index 0000000000000..6104758f28fd8 --- /dev/null +++ b/x-pack/plugins/uptime/common/runtime_types/network_events.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +const NetworkTimingsType = t.type({ + queueing: t.number, + connect: t.number, + total: t.number, + send: t.number, + blocked: t.number, + receive: t.number, + wait: t.number, + dns: t.number, + proxy: t.number, + ssl: t.number, +}); + +export type NetworkTimings = t.TypeOf; + +const NetworkEventType = t.intersection([ + t.type({ + timestamp: t.string, + requestSentTime: t.number, + loadEndTime: t.number, + }), + t.partial({ + method: t.string, + url: t.string, + status: t.number, + mimeType: t.string, + requestStartTime: t.number, + timings: NetworkTimingsType, + }), +]); + +export type NetworkEvent = t.TypeOf; + +export const SyntheticsNetworkEventsApiResponseType = t.type({ + events: t.array(NetworkEventType), +}); + +export type SyntheticsNetworkEventsApiResponse = t.TypeOf< + typeof SyntheticsNetworkEventsApiResponseType +>; diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts index 17b2d143eeab0..bbb6a8b915e05 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts @@ -240,10 +240,32 @@ export const PingType = t.intersection([ }), ]); -export const SyntheticsJourneyApiResponseType = t.type({ - checkGroup: t.string, - steps: t.array(PingType), -}); +export const SyntheticsJourneyApiResponseType = t.intersection([ + t.type({ + checkGroup: t.string, + steps: t.array(PingType), + }), + t.partial({ + details: t.union([ + t.intersection([ + t.type({ + timestamp: t.string, + }), + t.partial({ + next: t.type({ + timestamp: t.string, + checkGroup: t.string, + }), + previous: t.type({ + timestamp: t.string, + checkGroup: t.string, + }), + }), + ]), + t.null, + ]), + }), +]); export type SyntheticsJourneyApiResponse = t.TypeOf; diff --git a/x-pack/plugins/uptime/public/components/common/header/page_header.tsx b/x-pack/plugins/uptime/public/components/common/header/page_header.tsx index 63bcb6663619d..7d093efd31be0 100644 --- a/x-pack/plugins/uptime/public/components/common/header/page_header.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/page_header.tsx @@ -11,7 +11,12 @@ import { useRouteMatch } from 'react-router-dom'; import { UptimeDatePicker } from '../uptime_date_picker'; import { SyntheticsCallout } from '../../overview/synthetics_callout'; import { PageTabs } from './page_tabs'; -import { CERTIFICATES_ROUTE, MONITOR_ROUTE, SETTINGS_ROUTE } from '../../../../common/constants'; +import { + CERTIFICATES_ROUTE, + MONITOR_ROUTE, + SETTINGS_ROUTE, + STEP_DETAIL_ROUTE, +} from '../../../../common/constants'; import { CertRefreshBtn } from '../../certificates/cert_refresh_btn'; import { ToggleAlertFlyoutButton } from '../../overview/alerts/alerts_containers'; @@ -34,11 +39,12 @@ const StyledPicker = styled(EuiFlexItem)` export const PageHeader = () => { const isCertRoute = useRouteMatch(CERTIFICATES_ROUTE); const isSettingsRoute = useRouteMatch(SETTINGS_ROUTE); + const isStepDetailRoute = useRouteMatch(STEP_DETAIL_ROUTE); const DatePickerComponent = () => isCertRoute ? ( - ) : ( + ) : isStepDetailRoute ? null : ( diff --git a/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx b/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx new file mode 100644 index 0000000000000..a8e4c90f2d29a --- /dev/null +++ b/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiLink } from '@elastic/eui'; +import { Link } from 'react-router-dom'; + +interface StepDetailLinkProps { + /** + * The ID of the check group (the journey run) + */ + checkGroupId: string; + /** + * The index of the step + */ + stepIndex: number; +} + +export const StepDetailLink: FC = ({ children, checkGroupId, stepIndex }) => { + const to = `/journey/${checkGroupId}/step/${stepIndex}`; + + return ( + + + {children} + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx index d6f422b5c7094..030b1a49009ef 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx @@ -206,6 +206,7 @@ describe('ExecutedJourney component', () => { direction="column" > { } /> { let step: Ping; @@ -34,8 +34,11 @@ describe('ExecutedStep', () => { }); it('renders correct step heading', () => { - expect(mountWithIntl().find('EuiText')) - .toMatchInlineSnapshot(` + expect( + mountWithRouter().find( + 'EuiText' + ) + ).toMatchInlineSnapshot(`
{ `); }); + it('renders a link to the step detail view', () => { + expect( + mountWithRouter().find( + 'StepDetailLink' + ) + ).toMatchInlineSnapshot(` + + + + + + `); + }); + it('supplies status badge correct status', () => { step.synthetics = { payload: { status: 'THE_STATUS' }, }; - expect(shallowWithIntl().find('StatusBadge')) - .toMatchInlineSnapshot(` + expect( + mountWithRouter().find( + 'StatusBadge' + ) + ).toMatchInlineSnapshot(` + > + + + + + + + + `); }); @@ -86,8 +170,11 @@ describe('ExecutedStep', () => { }, }; - expect(shallowWithIntl().find('CodeBlockAccordion')) - .toMatchInlineSnapshot(` + expect( + mountWithRouter().find( + 'CodeBlockAccordion' + ) + ).toMatchInlineSnapshot(` Array [ { language="javascript" overflowHeight={360} > - const someVar = "the var" + +
+
+ +
+
+ +
+
+ + +
+
+                              
+                                const someVar = "the var"
+                              
+                            
+
+
+
+
+
+
+
+
+
, { language="html" overflowHeight={360} > - There was an error executing the step. + +
+
+ +
+
+ +
+
+ + +
+
+                              
+                                There was an error executing the step.
+                              
+                            
+
+
+
+
+
+
+
+
+
, { language="html" overflowHeight={360} > - some.stack.trace.string + +
+
+ +
+
+ +
+
+ + +
+
+                              
+                                some.stack.trace.string
+                              
+                            
+
+
+
+
+
+
+
+
+
, ] `); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx index 0c47e4c73e976..a9748524d1bb3 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx @@ -78,7 +78,7 @@ export const ExecutedJourney: FC = ({ journey }) => { {journey.steps.filter(isStepEnd).map((step, index) => ( - + ))} diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx index 5966851973af2..01a599f8e8a60 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx @@ -12,80 +12,104 @@ import { CodeBlockAccordion } from './code_block_accordion'; import { StepScreenshotDisplay } from './step_screenshot_display'; import { StatusBadge } from './status_badge'; import { Ping } from '../../../../common/runtime_types'; +import { StepDetailLink } from '../../common/step_detail_link'; const CODE_BLOCK_OVERFLOW_HEIGHT = 360; interface ExecutedStepProps { step: Ping; index: number; + checkGroup: string; } -export const ExecutedStep: FC = ({ step, index }) => ( - <> -
-
- - - - - +export const ExecutedStep: FC = ({ step, index, checkGroup }) => { + return ( + <> +
+
+ {step.synthetics?.step?.index && checkGroup ? ( + + + + + + + + ) : ( + + + + + + )} +
+ +
+ +
+ +
+ + + + + + + {step.synthetics?.payload?.source} + + + {step.synthetics?.error?.message} + + + {step.synthetics?.error?.stack} + + + +
- -
- -
- -
- - - - - - - {step.synthetics?.payload?.source} - - - {step.synthetics?.error?.message} - - - {step.synthetics?.error?.stack} - - - -
-
- -); + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail.tsx new file mode 100644 index 0000000000000..fd68edef3226b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail.tsx @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, + EuiButtonEmpty, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import moment from 'moment'; +import { WaterfallChartContainer } from './waterfall/waterfall_chart_container'; + +export const PREVIOUS_CHECK_BUTTON_TEXT = i18n.translate( + 'xpack.uptime.synthetics.stepDetail.previousCheckButtonText', + { + defaultMessage: 'Previous check', + } +); + +export const NEXT_CHECK_BUTTON_TEXT = i18n.translate( + 'xpack.uptime.synthetics.stepDetail.nextCheckButtonText', + { + defaultMessage: 'Next check', + } +); + +interface Props { + checkGroup: string; + stepName?: string; + stepIndex: number; + totalSteps: number; + hasPreviousStep: boolean; + hasNextStep: boolean; + handlePreviousStep: () => void; + handleNextStep: () => void; + handleNextRun: () => void; + handlePreviousRun: () => void; + previousCheckGroup?: string; + nextCheckGroup?: string; + checkTimestamp?: string; + dateFormat: string; +} + +export const StepDetail: React.FC = ({ + dateFormat, + stepName, + checkGroup, + stepIndex, + totalSteps, + hasPreviousStep, + hasNextStep, + handlePreviousStep, + handleNextStep, + handlePreviousRun, + handleNextRun, + previousCheckGroup, + nextCheckGroup, + checkTimestamp, +}) => { + return ( + <> + + + + + +

{stepName}

+
+
+ + + + + + + + + + + + + + + +
+
+ + + + + {PREVIOUS_CHECK_BUTTON_TEXT} + + + + {moment(checkTimestamp).format(dateFormat)} + + + + {NEXT_CHECK_BUTTON_TEXT} + + + + +
+ + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx new file mode 100644 index 0000000000000..58cf8d6e492da --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText, EuiLoadingSpinner } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useEffect, useCallback, useMemo } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import moment from 'moment'; +import { useBreadcrumbs } from '../../../../hooks/use_breadcrumbs'; +import { getJourneySteps } from '../../../../state/actions/journey'; +import { journeySelector } from '../../../../state/selectors'; +import { useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public'; +import { StepDetail } from './step_detail'; + +export const NO_STEP_DATA = i18n.translate('xpack.uptime.synthetics.stepDetail.noData', { + defaultMessage: 'No data could be found for this step', +}); + +interface Props { + checkGroup: string; + stepIndex: number; +} + +export const StepDetailContainer: React.FC = ({ checkGroup, stepIndex }) => { + const dispatch = useDispatch(); + const history = useHistory(); + + const [dateFormat] = useUiSetting$('dateFormat'); + + useEffect(() => { + if (checkGroup) { + dispatch(getJourneySteps({ checkGroup })); + } + }, [dispatch, checkGroup]); + + const journeys = useSelector(journeySelector); + const journey = journeys[checkGroup ?? '']; + + const { activeStep, hasPreviousStep, hasNextStep } = useMemo(() => { + return { + hasPreviousStep: stepIndex > 1 ? true : false, + activeStep: journey?.steps?.find((step) => step.synthetics?.step?.index === stepIndex), + hasNextStep: journey && journey.steps && stepIndex < journey.steps.length ? true : false, + }; + }, [stepIndex, journey]); + + useBreadcrumbs([ + ...(activeStep?.monitor?.name ? [{ text: activeStep?.monitor?.name }] : []), + ...(journey?.details?.timestamp + ? [{ text: moment(journey?.details?.timestamp).format(dateFormat) }] + : []), + ]); + + const handleNextStep = useCallback(() => { + history.push(`/journey/${checkGroup}/step/${stepIndex + 1}`); + }, [history, checkGroup, stepIndex]); + + const handlePreviousStep = useCallback(() => { + history.push(`/journey/${checkGroup}/step/${stepIndex - 1}`); + }, [history, checkGroup, stepIndex]); + + const handleNextRun = useCallback(() => { + history.push(`/journey/${journey?.details?.next?.checkGroup}/step/1`); + }, [history, journey?.details?.next?.checkGroup]); + + const handlePreviousRun = useCallback(() => { + history.push(`/journey/${journey?.details?.previous?.checkGroup}/step/1`); + }, [history, journey?.details?.previous?.checkGroup]); + + return ( + <> + + {(!journey || journey.loading) && ( + + + + + + )} + {journey && !activeStep && !journey.loading && ( + + + +

{NO_STEP_DATA}

+
+
+
+ )} + {journey && activeStep && !journey.loading && ( + + )} +
+ + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts new file mode 100644 index 0000000000000..fff14376667b2 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { colourPalette } from './data_formatting'; + +describe('Palettes', () => { + it('A colour palette comprising timing and mime type colours is correctly generated', () => { + expect(colourPalette).toEqual({ + blocked: '#b9a888', + connect: '#da8b45', + dns: '#54b399', + font: '#aa6556', + html: '#f3b3a6', + media: '#d6bf57', + other: '#e7664c', + receive: '#54b399', + script: '#9170b8', + send: '#d36086', + ssl: '#edc5a2', + stylesheet: '#ca8eae', + wait: '#b0c9e0', + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts new file mode 100644 index 0000000000000..7c6e176315b5b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts @@ -0,0 +1,209 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { euiPaletteColorBlind } from '@elastic/eui'; + +import { + NetworkItems, + NetworkItem, + FriendlyTimingLabels, + FriendlyMimetypeLabels, + MimeType, + MimeTypesMap, + Timings, + TIMING_ORDER, + SidebarItems, + LegendItems, +} from './types'; +import { WaterfallData } from '../../waterfall'; +import { NetworkEvent } from '../../../../../../common/runtime_types'; + +export const extractItems = (data: NetworkEvent[]): NetworkItems => { + // NOTE: This happens client side as the "payload" property is mapped + // in such a way it can't be queried (or sorted on) via ES. + return data.sort((a: NetworkItem, b: NetworkItem) => { + return a.requestSentTime - b.requestSentTime; + }); +}; + +const formatValueForDisplay = (value: number, points: number = 3) => { + return Number(value).toFixed(points); +}; + +const getColourForMimeType = (mimeType?: string) => { + const key = mimeType && MimeTypesMap[mimeType] ? MimeTypesMap[mimeType] : MimeType.Other; + return colourPalette[key]; +}; + +export const getSeriesAndDomain = (items: NetworkItems) => { + const getValueForOffset = (item: NetworkItem) => { + return item.requestSentTime; + }; + + // The earliest point in time a request is sent or started. This will become our notion of "0". + const zeroOffset = items.reduce((acc, item) => { + const offsetValue = getValueForOffset(item); + return offsetValue < acc ? offsetValue : acc; + }, Infinity); + + const getValue = (timings: NetworkEvent['timings'], timing: Timings) => { + if (!timings) return; + + // SSL is a part of the connect timing + if (timing === Timings.Connect && timings.ssl > 0) { + return timings.connect - timings.ssl; + } else { + return timings[timing]; + } + }; + + const series = items.reduce((acc, item, index) => { + if (!item.timings) return acc; + + const offsetValue = getValueForOffset(item); + + let currentOffset = offsetValue - zeroOffset; + + TIMING_ORDER.forEach((timing) => { + const value = getValue(item.timings, timing); + const colour = + timing === Timings.Receive ? getColourForMimeType(item.mimeType) : colourPalette[timing]; + if (value && value >= 0) { + const y = currentOffset + value; + + acc.push({ + x: index, + y0: currentOffset, + y, + config: { + colour, + tooltipProps: { + value: `${FriendlyTimingLabels[timing]}: ${formatValueForDisplay( + y - currentOffset + )}ms`, + colour, + }, + }, + }); + currentOffset = y; + } + }); + return acc; + }, []); + + const yValues = series.map((serie) => serie.y); + const domain = { min: 0, max: Math.max(...yValues) }; + return { series, domain }; +}; + +export const getSidebarItems = (items: NetworkItems): SidebarItems => { + return items.map((item) => { + const { url, status, method } = item; + return { url, status, method }; + }); +}; + +export const getLegendItems = (): LegendItems => { + let timingItems: LegendItems = []; + Object.values(Timings).forEach((timing) => { + // The "receive" timing is mapped to a mime type colour, so we don't need to show this in the legend + if (timing === Timings.Receive) { + return; + } + timingItems = [ + ...timingItems, + { name: FriendlyTimingLabels[timing], colour: TIMING_PALETTE[timing] }, + ]; + }); + + let mimeTypeItems: LegendItems = []; + Object.values(MimeType).forEach((mimeType) => { + mimeTypeItems = [ + ...mimeTypeItems, + { name: FriendlyMimetypeLabels[mimeType], colour: MIME_TYPE_PALETTE[mimeType] }, + ]; + }); + return [...timingItems, ...mimeTypeItems]; +}; + +// Timing colour palette +type TimingColourPalette = { + [K in Timings]: string; +}; + +const SAFE_PALETTE = euiPaletteColorBlind({ rotations: 2 }); + +const buildTimingPalette = (): TimingColourPalette => { + const palette = Object.values(Timings).reduce>((acc, value) => { + switch (value) { + case Timings.Blocked: + acc[value] = SAFE_PALETTE[6]; + break; + case Timings.Dns: + acc[value] = SAFE_PALETTE[0]; + break; + case Timings.Connect: + acc[value] = SAFE_PALETTE[7]; + break; + case Timings.Ssl: + acc[value] = SAFE_PALETTE[17]; + break; + case Timings.Send: + acc[value] = SAFE_PALETTE[2]; + break; + case Timings.Wait: + acc[value] = SAFE_PALETTE[11]; + break; + case Timings.Receive: + acc[value] = SAFE_PALETTE[0]; + break; + } + return acc; + }, {}); + + return palette as TimingColourPalette; +}; + +const TIMING_PALETTE = buildTimingPalette(); + +// MimeType colour palette +type MimeTypeColourPalette = { + [K in MimeType]: string; +}; + +const buildMimeTypePalette = (): MimeTypeColourPalette => { + const palette = Object.values(MimeType).reduce>((acc, value) => { + switch (value) { + case MimeType.Html: + acc[value] = SAFE_PALETTE[19]; + break; + case MimeType.Script: + acc[value] = SAFE_PALETTE[3]; + break; + case MimeType.Stylesheet: + acc[value] = SAFE_PALETTE[4]; + break; + case MimeType.Media: + acc[value] = SAFE_PALETTE[5]; + break; + case MimeType.Font: + acc[value] = SAFE_PALETTE[8]; + break; + case MimeType.Other: + acc[value] = SAFE_PALETTE[9]; + break; + } + return acc; + }, {}); + + return palette as MimeTypeColourPalette; +}; + +const MIME_TYPE_PALETTE = buildMimeTypePalette(); + +type ColourPalette = TimingColourPalette & MimeTypeColourPalette; + +export const colourPalette: ColourPalette = { ...TIMING_PALETTE, ...MIME_TYPE_PALETTE }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/types.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts similarity index 86% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/types.ts rename to x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts index 1dd58b4f86db3..738929741ddaf 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/types.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { NetworkEvent } from '../../../../../../common/runtime_types'; export enum Timings { Blocked = 'blocked', @@ -33,7 +34,7 @@ export const FriendlyTimingLabels = { } ), [Timings.Ssl]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.timings.ssl', { - defaultMessage: 'SSL', + defaultMessage: 'TLS', }), [Timings.Send]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.timings.send', { defaultMessage: 'Sending request', @@ -144,21 +145,7 @@ export const MimeTypesMap: Record = { 'application/font-sfnt': MimeType.Font, }; -export interface NetworkItem { - timestamp: string; - method: string; - url: string; - status: number; - mimeType?: string; - // NOTE: This is the time the request was actually issued. timing.request_time might be later if the request was queued. - requestSentTime: number; - responseReceivedTime: number; - // NOTE: Denotes the earlier figure out of request sent time and request start time (part of timings). This can vary based on queue times, and - // also whether an entry actually has timings available. - // Ref: https://github.com/ChromeDevTools/devtools-frontend/blob/ed2a064ac194bfae4e25c4748a9fa3513b3e9f7d/front_end/network/RequestTimingView.js#L154 - earliestRequestTime: number; - timings: CalculatedTimings | null; -} +export type NetworkItem = NetworkEvent; export type NetworkItems = NetworkItem[]; // NOTE: A number will always be present if the property exists, but that number might be -1, which represents no value. diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx new file mode 100644 index 0000000000000..7657ca7f9c64a --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiLoadingChart } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useEffect } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { getNetworkEvents } from '../../../../../state/actions/network_events'; +import { networkEventsSelector } from '../../../../../state/selectors'; +import { WaterfallChartWrapper } from './waterfall_chart_wrapper'; +import { extractItems } from './data_formatting'; + +export const NO_DATA_TEXT = i18n.translate('xpack.uptime.synthetics.stepDetail.waterfallNoData', { + defaultMessage: 'No waterfall data could be found for this step', +}); + +interface Props { + checkGroup: string; + stepIndex: number; +} + +export const WaterfallChartContainer: React.FC = ({ checkGroup, stepIndex }) => { + const dispatch = useDispatch(); + + useEffect(() => { + if (checkGroup && stepIndex) { + dispatch( + getNetworkEvents({ + checkGroup, + stepIndex, + }) + ); + } + }, [dispatch, stepIndex, checkGroup]); + + const _networkEvents = useSelector(networkEventsSelector); + const networkEvents = _networkEvents[checkGroup ?? '']?.[stepIndex]; + + return ( + <> + {!networkEvents || + (networkEvents.loading && ( + + + + + + ))} + {networkEvents && !networkEvents.loading && networkEvents.events.length === 0 && ( + + + +

{NO_DATA_TEXT}

+
+
+
+ )} + {networkEvents && !networkEvents.loading && networkEvents.events.length > 0 && ( + + )} + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/waterfall_chart_wrapper.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx similarity index 91% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/waterfall_chart_wrapper.tsx rename to x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx index 434b44a94b79f..b10c3844f3002 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/waterfall_chart_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx @@ -13,7 +13,7 @@ import { WaterfallChart, MiddleTruncatedText, RenderItem, -} from '../../../waterfall'; +} from '../../waterfall'; const renderSidebarItem: RenderItem = (item, index) => { const { status } = item; @@ -27,7 +27,7 @@ const renderSidebarItem: RenderItem = (item, index) => { return ( <> - {!isErrorStatusCode(status) ? ( + {!status || !isErrorStatusCode(status) ? ( ) : ( @@ -47,9 +47,12 @@ const renderLegendItem: RenderItem = (item) => { return {item.name}; }; -export const WaterfallChartWrapper = () => { - // TODO: Will be sourced via an API - const [networkData] = useState([]); +interface Props { + data: NetworkItems; +} + +export const WaterfallChartWrapper: React.FC = ({ data }) => { + const [networkData] = useState(data); const { series, domain } = useMemo(() => { return getSeriesAndDomain(networkData); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts index ac650c5ef0ddd..95ec298e2e349 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts @@ -10,3 +10,6 @@ export const BAR_HEIGHT = 32; export const MAIN_GROW_SIZE = 8; // Flex grow value export const SIDEBAR_GROW_SIZE = 2; +// Axis height +// NOTE: This isn't a perfect solution - changes in font size etc within charts could change the ideal height here. +export const FIXED_AXIS_HEIGHT = 32; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx index 9ff544fc1946b..c551561d5ad4f 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/sidebar.tsx @@ -27,7 +27,11 @@ export const Sidebar: React.FC = ({ items, height, render }) => { - + {items.map((item, index) => { return ( diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts index 25f5e5f8f5cc9..320e415585ca3 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts @@ -6,9 +6,7 @@ import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { euiStyled } from '../../../../../../../observability/public'; - -// NOTE: This isn't a perfect solution - changes in font size etc within charts could change the ideal height here. -const FIXED_AXIS_HEIGHT = 33; +import { FIXED_AXIS_HEIGHT } from './constants'; interface WaterfallChartOuterContainerProps { height?: number; @@ -24,6 +22,7 @@ export const WaterfallChartFixedTopContainer = euiStyled.div` position: sticky; top: 0; z-index: ${(props) => props.theme.eui.euiZLevel4}; + border-bottom: ${(props) => `1px solid ${props.theme.eui.euiColorLightShade}`}; `; export const WaterfallChartFixedTopContainerSidebarCover = euiStyled(EuiPanel)` diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx index de4be0ea34b2c..d92e26335a6bd 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx @@ -33,7 +33,7 @@ import { WaterfallChartTooltip, } from './styles'; import { WaterfallData } from '../types'; -import { BAR_HEIGHT, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE } from './constants'; +import { BAR_HEIGHT, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE, FIXED_AXIS_HEIGHT } from './constants'; import { Sidebar } from './sidebar'; import { Legend } from './legend'; @@ -77,7 +77,8 @@ const getUniqueBars = (data: WaterfallData) => { }, new Set()); }; -const getChartHeight = (data: WaterfallData): number => getUniqueBars(data).size * BAR_HEIGHT; +const getChartHeight = (data: WaterfallData): number => + getUniqueBars(data).size * BAR_HEIGHT + FIXED_AXIS_HEIGHT; export const WaterfallChart = ({ tickFormat, @@ -85,7 +86,7 @@ export const WaterfallChart = ({ barStyleAccessor, renderSidebarItem, renderLegendItem, - maxHeight = 600, + maxHeight = 800, }: WaterfallChartProps) => { const { data, sidebarItems, legendItems } = useWaterfallContext(); @@ -108,10 +109,10 @@ export const WaterfallChart = ({ <> - + {shouldRenderSidebar && ( - + )} @@ -130,10 +131,13 @@ export const WaterfallChart = ({ tickFormat={tickFormat} domain={domain} showGridLines={true} + style={{ + axisLine: { + visible: false, + }, + }} /> - ''} /> - - + {shouldRenderSidebar && ( )} @@ -169,10 +173,13 @@ export const WaterfallChart = ({ tickFormat={tickFormat} domain={domain} showGridLines={true} + style={{ + axisLine: { + visible: false, + }, + }} /> - ''} /> - seconds * 1000; - -// describe('getTimings', () => { -// it('Calculates timings for network events correctly', () => { -// // NOTE: Uses these timings as the file protocol events don't have timing information -// const eventOneTimings = getTimings( -// TEST_DATA[0].synthetics.payload.response.timing!, -// toMillis(TEST_DATA[0].synthetics.payload.start), -// toMillis(TEST_DATA[0].synthetics.payload.end) -// ); -// expect(eventOneTimings).toEqual({ -// blocked: 162.4549999999106, -// connect: -1, -// dns: -1, -// receive: 0.5629999989271255, -// send: 0.5149999999999864, -// ssl: undefined, -// wait: 28.494, -// }); - -// const eventFourTimings = getTimings( -// TEST_DATA[3].synthetics.payload.response.timing!, -// toMillis(TEST_DATA[3].synthetics.payload.start), -// toMillis(TEST_DATA[3].synthetics.payload.end) -// ); -// expect(eventFourTimings).toEqual({ -// blocked: 1.8559999997466803, -// connect: 25.52200000000002, -// dns: 4.683999999999999, -// receive: 0.6780000009983667, -// send: 0.6490000000000009, -// ssl: 130.541, -// wait: 27.245000000000005, -// }); -// }); -// }); - -// describe('getSeriesAndDomain', () => { -// let seriesAndDomain: any; -// let NetworkItems: any; - -// beforeAll(() => { -// NetworkItems = extractItems(TEST_DATA); -// seriesAndDomain = getSeriesAndDomain(NetworkItems); -// }); - -// it('Correctly calculates the domain', () => { -// expect(seriesAndDomain.domain).toEqual({ max: 218.34699999913573, min: 0 }); -// }); - -// it('Correctly calculates the series', () => { -// expect(seriesAndDomain.series).toEqual([ -// { -// config: { colour: '#f3b3a6', tooltipProps: { colour: '#f3b3a6', value: '3.635ms' } }, -// x: 0, -// y: 3.6349999997764826, -// y0: 0, -// }, -// { -// config: { -// colour: '#b9a888', -// tooltipProps: { colour: '#b9a888', value: 'Queued / Blocked: 1.856ms' }, -// }, -// x: 1, -// y: 27.889999999731778, -// y0: 26.0339999999851, -// }, -// { -// config: { colour: '#54b399', tooltipProps: { colour: '#54b399', value: 'DNS: 4.684ms' } }, -// x: 1, -// y: 32.573999999731775, -// y0: 27.889999999731778, -// }, -// { -// config: { -// colour: '#da8b45', -// tooltipProps: { colour: '#da8b45', value: 'Connecting: 25.522ms' }, -// }, -// x: 1, -// y: 58.095999999731795, -// y0: 32.573999999731775, -// }, -// { -// config: { colour: '#edc5a2', tooltipProps: { colour: '#edc5a2', value: 'SSL: 130.541ms' } }, -// x: 1, -// y: 188.63699999973178, -// y0: 58.095999999731795, -// }, -// { -// config: { -// colour: '#d36086', -// tooltipProps: { colour: '#d36086', value: 'Sending request: 0.649ms' }, -// }, -// x: 1, -// y: 189.28599999973179, -// y0: 188.63699999973178, -// }, -// { -// config: { -// colour: '#b0c9e0', -// tooltipProps: { colour: '#b0c9e0', value: 'Waiting (TTFB): 27.245ms' }, -// }, -// x: 1, -// y: 216.5309999997318, -// y0: 189.28599999973179, -// }, -// { -// config: { -// colour: '#ca8eae', -// tooltipProps: { colour: '#ca8eae', value: 'Content downloading: 0.678ms' }, -// }, -// x: 1, -// y: 217.20900000073016, -// y0: 216.5309999997318, -// }, -// { -// config: { -// colour: '#b9a888', -// tooltipProps: { colour: '#b9a888', value: 'Queued / Blocked: 162.455ms' }, -// }, -// x: 2, -// y: 188.77500000020862, -// y0: 26.320000000298023, -// }, -// { -// config: { -// colour: '#d36086', -// tooltipProps: { colour: '#d36086', value: 'Sending request: 0.515ms' }, -// }, -// x: 2, -// y: 189.2900000002086, -// y0: 188.77500000020862, -// }, -// { -// config: { -// colour: '#b0c9e0', -// tooltipProps: { colour: '#b0c9e0', value: 'Waiting (TTFB): 28.494ms' }, -// }, -// x: 2, -// y: 217.7840000002086, -// y0: 189.2900000002086, -// }, -// { -// config: { -// colour: '#9170b8', -// tooltipProps: { colour: '#9170b8', value: 'Content downloading: 0.563ms' }, -// }, -// x: 2, -// y: 218.34699999913573, -// y0: 217.7840000002086, -// }, -// { -// config: { colour: '#9170b8', tooltipProps: { colour: '#9170b8', value: '12.139ms' } }, -// x: 3, -// y: 46.15699999965727, -// y0: 34.01799999922514, -// }, -// { -// config: { colour: '#9170b8', tooltipProps: { colour: '#9170b8', value: '8.453ms' } }, -// x: 4, -// y: 43.506999999284744, -// y0: 35.053999999538064, -// }, -// ]); -// }); -// }); - -describe('Palettes', () => { - it('A colour palette comprising timing and mime type colours is correctly generated', () => { - expect(colourPalette).toEqual({ - blocked: '#b9a888', - connect: '#da8b45', - dns: '#54b399', - font: '#aa6556', - html: '#f3b3a6', - media: '#d6bf57', - other: '#e7664c', - receive: '#54b399', - script: '#9170b8', - send: '#d36086', - ssl: '#edc5a2', - stylesheet: '#ca8eae', - wait: '#b0c9e0', - }); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.ts deleted file mode 100644 index 9c66ea638c942..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/consumers/synthetics/data_formatting.ts +++ /dev/null @@ -1,336 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { euiPaletteColorBlind } from '@elastic/eui'; - -import { - PayloadTimings, - CalculatedTimings, - NetworkItems, - FriendlyTimingLabels, - FriendlyMimetypeLabels, - MimeType, - MimeTypesMap, - Timings, - TIMING_ORDER, - SidebarItems, - LegendItems, -} from './types'; -import { WaterfallData } from '../../../waterfall'; - -const microToMillis = (micro: number): number => (micro === -1 ? -1 : micro * 1000); - -// The timing calculations here are based off several sources: -// https://github.com/ChromeDevTools/devtools-frontend/blob/2fe91adefb2921b4deb2b4b125370ef9ccdb8d1b/front_end/sdk/HARLog.js#L307 -// and -// https://chromium.googlesource.com/chromium/blink.git/+/master/Source/devtools/front_end/sdk/HAREntry.js#131 -// and -// https://github.com/cyrus-and/chrome-har-capturer/blob/master/lib/har.js#L195 -// Order of events: request_start = 0, [proxy], [dns], [connect [ssl]], [send], receive_headers_end - -export const getTimings = ( - timings: PayloadTimings, - requestSentTime: number, - responseReceivedTime: number -): CalculatedTimings => { - if (!timings) return { blocked: -1, dns: -1, connect: -1, send: 0, wait: 0, receive: 0, ssl: -1 }; - - const getLeastNonNegative = (values: number[]) => - values.reduce((best, value) => (value >= 0 && value < best ? value : best), Infinity); - const getOptionalTiming = (_timings: PayloadTimings, key: keyof PayloadTimings) => - _timings[key] >= 0 ? _timings[key] : -1; - - // NOTE: Request sent and request start can differ due to queue times - const requestStartTime = microToMillis(timings.request_time); - - // Queued - const queuedTime = requestSentTime < requestStartTime ? requestStartTime - requestSentTime : -1; - - // Blocked - // "blocked" represents both queued time + blocked/stalled time + proxy time (ie: anything before the request was actually started). - let blocked = queuedTime; - - const blockedStart = getLeastNonNegative([ - timings.dns_start, - timings.connect_start, - timings.send_start, - ]); - - if (blockedStart !== Infinity) { - blocked += blockedStart; - } - - // Proxy - // Proxy is part of blocked, but it can be quirky in that blocked can be -1 even though there are proxy timings. This can happen with - // protocols like Quic. - if (timings.proxy_end !== -1) { - const blockedProxy = timings.proxy_end - timings.proxy_start; - - if (blockedProxy && blockedProxy > blocked) { - blocked = blockedProxy; - } - } - - // DNS - const dnsStart = timings.dns_end >= 0 ? blockedStart : 0; - const dnsEnd = getOptionalTiming(timings, 'dns_end'); - const dns = dnsEnd - dnsStart; - - // SSL - const sslStart = getOptionalTiming(timings, 'ssl_start'); - const sslEnd = getOptionalTiming(timings, 'ssl_end'); - let ssl; - - if (sslStart >= 0 && sslEnd >= 0) { - ssl = timings.ssl_end - timings.ssl_start; - } - - // Connect - let connect = -1; - if (timings.connect_start >= 0) { - connect = timings.send_start - timings.connect_start; - } - - // Send - const send = timings.send_end - timings.send_start; - - // Wait - const wait = timings.receive_headers_end - timings.send_end; - - // Receive - const receive = responseReceivedTime - (requestStartTime + timings.receive_headers_end); - - // SSL connection is a part of the overall connection time - if (connect && ssl) { - connect = connect - ssl; - } - - return { blocked, dns, connect, send, wait, receive, ssl }; -}; - -// TODO: Switch to real API data, and type data as the payload response (if server response isn't preformatted) -export const extractItems = (data: any): NetworkItems => { - const items = data - .map((entry: any) => { - const requestSentTime = microToMillis(entry.synthetics.payload.start); - const responseReceivedTime = microToMillis(entry.synthetics.payload.end); - const requestStartTime = - entry.synthetics.payload.response && entry.synthetics.payload.response.timing - ? microToMillis(entry.synthetics.payload.response.timing.request_time) - : null; - - return { - timestamp: entry['@timestamp'], - method: entry.synthetics.payload.method, - url: entry.synthetics.payload.url, - status: entry.synthetics.payload.status, - mimeType: entry.synthetics.payload?.response?.mime_type, - requestSentTime, - responseReceivedTime, - earliestRequestTime: requestStartTime - ? Math.min(requestSentTime, requestStartTime) - : requestSentTime, - timings: - entry.synthetics.payload.response && entry.synthetics.payload.response.timing - ? getTimings( - entry.synthetics.payload.response.timing, - requestSentTime, - responseReceivedTime - ) - : null, - }; - }) - .sort((a: any, b: any) => { - return a.earliestRequestTime - b.earliestRequestTime; - }); - - return items; -}; - -const formatValueForDisplay = (value: number, points: number = 3) => { - return Number(value).toFixed(points); -}; - -const getColourForMimeType = (mimeType?: string) => { - const key = mimeType && MimeTypesMap[mimeType] ? MimeTypesMap[mimeType] : MimeType.Other; - return colourPalette[key]; -}; - -export const getSeriesAndDomain = (items: NetworkItems) => { - // The earliest point in time a request is sent or started. This will become our notion of "0". - const zeroOffset = items.reduce((acc, item) => { - const { earliestRequestTime } = item; - return earliestRequestTime < acc ? earliestRequestTime : acc; - }, Infinity); - - const series = items.reduce((acc, item, index) => { - const { earliestRequestTime } = item; - - // Entries without timings should be handled differently: - // https://github.com/ChromeDevTools/devtools-frontend/blob/ed2a064ac194bfae4e25c4748a9fa3513b3e9f7d/front_end/network/RequestTimingView.js#L140 - // If there are no concrete timings just plot one block via start and end - if (!item.timings || item.timings === null) { - const duration = item.responseReceivedTime - item.earliestRequestTime; - const colour = getColourForMimeType(item.mimeType); - return [ - ...acc, - { - x: index, - y0: item.earliestRequestTime - zeroOffset, - y: item.responseReceivedTime - zeroOffset, - config: { - colour, - tooltipProps: { - value: `${formatValueForDisplay(duration)}ms`, - colour, - }, - }, - }, - ]; - } - - let currentOffset = earliestRequestTime - zeroOffset; - - TIMING_ORDER.forEach((timing) => { - const value = item.timings![timing]; - const colour = - timing === Timings.Receive ? getColourForMimeType(item.mimeType) : colourPalette[timing]; - if (value && value >= 0) { - const y = currentOffset + value; - - acc.push({ - x: index, - y0: currentOffset, - y, - config: { - colour, - tooltipProps: { - value: `${FriendlyTimingLabels[timing]}: ${formatValueForDisplay( - y - currentOffset - )}ms`, - colour, - }, - }, - }); - currentOffset = y; - } - }); - return acc; - }, []); - - const yValues = series.map((serie) => serie.y); - const domain = { min: 0, max: Math.max(...yValues) }; - return { series, domain }; -}; - -export const getSidebarItems = (items: NetworkItems): SidebarItems => { - return items.map((item) => { - const { url, status, method } = item; - return { url, status, method }; - }); -}; - -export const getLegendItems = (): LegendItems => { - let timingItems: LegendItems = []; - Object.values(Timings).forEach((timing) => { - // The "receive" timing is mapped to a mime type colour, so we don't need to show this in the legend - if (timing === Timings.Receive) { - return; - } - timingItems = [ - ...timingItems, - { name: FriendlyTimingLabels[timing], colour: TIMING_PALETTE[timing] }, - ]; - }); - - let mimeTypeItems: LegendItems = []; - Object.values(MimeType).forEach((mimeType) => { - mimeTypeItems = [ - ...mimeTypeItems, - { name: FriendlyMimetypeLabels[mimeType], colour: MIME_TYPE_PALETTE[mimeType] }, - ]; - }); - return [...timingItems, ...mimeTypeItems]; -}; - -// Timing colour palette -type TimingColourPalette = { - [K in Timings]: string; -}; - -const SAFE_PALETTE = euiPaletteColorBlind({ rotations: 2 }); - -const buildTimingPalette = (): TimingColourPalette => { - const palette = Object.values(Timings).reduce>((acc, value) => { - switch (value) { - case Timings.Blocked: - acc[value] = SAFE_PALETTE[6]; - break; - case Timings.Dns: - acc[value] = SAFE_PALETTE[0]; - break; - case Timings.Connect: - acc[value] = SAFE_PALETTE[7]; - break; - case Timings.Ssl: - acc[value] = SAFE_PALETTE[17]; - break; - case Timings.Send: - acc[value] = SAFE_PALETTE[2]; - break; - case Timings.Wait: - acc[value] = SAFE_PALETTE[11]; - break; - case Timings.Receive: - acc[value] = SAFE_PALETTE[0]; - break; - } - return acc; - }, {}); - - return palette as TimingColourPalette; -}; - -const TIMING_PALETTE = buildTimingPalette(); - -// MimeType colour palette -type MimeTypeColourPalette = { - [K in MimeType]: string; -}; - -const buildMimeTypePalette = (): MimeTypeColourPalette => { - const palette = Object.values(MimeType).reduce>((acc, value) => { - switch (value) { - case MimeType.Html: - acc[value] = SAFE_PALETTE[19]; - break; - case MimeType.Script: - acc[value] = SAFE_PALETTE[3]; - break; - case MimeType.Stylesheet: - acc[value] = SAFE_PALETTE[4]; - break; - case MimeType.Media: - acc[value] = SAFE_PALETTE[5]; - break; - case MimeType.Font: - acc[value] = SAFE_PALETTE[8]; - break; - case MimeType.Other: - acc[value] = SAFE_PALETTE[9]; - break; - } - return acc; - }, {}); - - return palette as MimeTypeColourPalette; -}; - -const MIME_TYPE_PALETTE = buildMimeTypePalette(); - -type ColourPalette = TimingColourPalette & MimeTypeColourPalette; - -export const colourPalette: ColourPalette = { ...TIMING_PALETTE, ...MIME_TYPE_PALETTE }; diff --git a/x-pack/plugins/uptime/public/hooks/use_telemetry.ts b/x-pack/plugins/uptime/public/hooks/use_telemetry.ts index 9b4a441fe5ade..21665e93dd274 100644 --- a/x-pack/plugins/uptime/public/hooks/use_telemetry.ts +++ b/x-pack/plugins/uptime/public/hooks/use_telemetry.ts @@ -14,6 +14,7 @@ export enum UptimePage { Monitor = 'Monitor', Settings = 'Settings', Certificates = 'Certificates', + StepDetail = 'StepDetail', NotFound = '__not-found__', } diff --git a/x-pack/plugins/uptime/public/pages/index.ts b/x-pack/plugins/uptime/public/pages/index.ts index cea47d6ccf79c..cb95fb8558cfb 100644 --- a/x-pack/plugins/uptime/public/pages/index.ts +++ b/x-pack/plugins/uptime/public/pages/index.ts @@ -5,5 +5,6 @@ */ export { MonitorPage } from './monitor'; +export { StepDetailPage } from './step_detail_page'; export { SettingsPage } from './settings'; export { NotFoundPage } from './not_found'; diff --git a/x-pack/plugins/uptime/public/pages/step_detail_page.tsx b/x-pack/plugins/uptime/public/pages/step_detail_page.tsx new file mode 100644 index 0000000000000..5bacad7e9a2d2 --- /dev/null +++ b/x-pack/plugins/uptime/public/pages/step_detail_page.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { useTrackPageview } from '../../../observability/public'; +import { useInitApp } from '../hooks/use_init_app'; +import { StepDetailContainer } from '../components/monitor/synthetics/step_detail/step_detail_container'; + +export const StepDetailPage: React.FC = () => { + useInitApp(); + const { checkGroupId, stepIndex } = useParams<{ checkGroupId: string; stepIndex: string }>(); + useTrackPageview({ app: 'uptime', path: 'stepDetail' }); + useTrackPageview({ app: 'uptime', path: 'stepDetail', delay: 15000 }); + + return ; +}; diff --git a/x-pack/plugins/uptime/public/routes.tsx b/x-pack/plugins/uptime/public/routes.tsx index 9b54c52cc674c..65526f9bca4fc 100644 --- a/x-pack/plugins/uptime/public/routes.tsx +++ b/x-pack/plugins/uptime/public/routes.tsx @@ -12,8 +12,9 @@ import { MONITOR_ROUTE, OVERVIEW_ROUTE, SETTINGS_ROUTE, + STEP_DETAIL_ROUTE, } from '../common/constants'; -import { MonitorPage, NotFoundPage, SettingsPage } from './pages'; +import { MonitorPage, StepDetailPage, NotFoundPage, SettingsPage } from './pages'; import { CertificatesPage } from './pages/certificates'; import { UptimePage, useUptimeTelemetry } from './hooks'; import { PageHeader } from './components/common/header/page_header'; @@ -50,6 +51,13 @@ const Routes: RouteProps[] = [ dataTestSubj: 'uptimeCertificatesPage', telemetryId: UptimePage.Certificates, }, + { + title: baseTitle, + path: STEP_DETAIL_ROUTE, + component: StepDetailPage, + dataTestSubj: 'uptimeStepDetailPage', + telemetryId: UptimePage.StepDetail, + }, { title: baseTitle, path: OVERVIEW_ROUTE, diff --git a/x-pack/plugins/uptime/public/state/actions/network_events.ts b/x-pack/plugins/uptime/public/state/actions/network_events.ts new file mode 100644 index 0000000000000..e3564689fcd48 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/actions/network_events.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createAction } from 'redux-actions'; +import { SyntheticsNetworkEventsApiResponse } from '../../../common/runtime_types'; + +export interface FetchNetworkEventsParams { + checkGroup: string; + stepIndex: number; +} + +export interface FetchNetworkEventsFailPayload { + checkGroup: string; + stepIndex: number; + error: Error; +} + +export const getNetworkEvents = createAction('GET_NETWORK_EVENTS'); +export const getNetworkEventsSuccess = createAction< + Pick & SyntheticsNetworkEventsApiResponse +>('GET_NETWORK_EVENTS_SUCCESS'); +export const getNetworkEventsFail = createAction( + 'GET_NETWORK_EVENTS_FAIL' +); diff --git a/x-pack/plugins/uptime/public/state/api/network_events.ts b/x-pack/plugins/uptime/public/state/api/network_events.ts new file mode 100644 index 0000000000000..a4eceb4812d28 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/api/network_events.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { apiService } from './utils'; +import { FetchNetworkEventsParams } from '../actions/network_events'; +import { + SyntheticsNetworkEventsApiResponse, + SyntheticsNetworkEventsApiResponseType, +} from '../../../common/runtime_types'; + +export async function fetchNetworkEvents( + params: FetchNetworkEventsParams +): Promise { + return (await apiService.get( + `/api/uptime/network_events`, + { + checkGroup: params.checkGroup, + stepIndex: params.stepIndex, + }, + SyntheticsNetworkEventsApiResponseType + )) as SyntheticsNetworkEventsApiResponse; +} diff --git a/x-pack/plugins/uptime/public/state/effects/index.ts b/x-pack/plugins/uptime/public/state/effects/index.ts index 4951f2102c8a7..3c75e75871882 100644 --- a/x-pack/plugins/uptime/public/state/effects/index.ts +++ b/x-pack/plugins/uptime/public/state/effects/index.ts @@ -19,6 +19,7 @@ import { fetchIndexStatusEffect } from './index_status'; import { fetchCertificatesEffect } from '../certificates/certificates'; import { fetchAlertsEffect } from '../alerts/alerts'; import { fetchJourneyStepsEffect } from './journey'; +import { fetchNetworkEventsEffect } from './network_events'; export function* rootEffect() { yield fork(fetchMonitorDetailsEffect); @@ -37,4 +38,5 @@ export function* rootEffect() { yield fork(fetchCertificatesEffect); yield fork(fetchAlertsEffect); yield fork(fetchJourneyStepsEffect); + yield fork(fetchNetworkEventsEffect); } diff --git a/x-pack/plugins/uptime/public/state/effects/network_events.ts b/x-pack/plugins/uptime/public/state/effects/network_events.ts new file mode 100644 index 0000000000000..95d24fbe37ae2 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/effects/network_events.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from 'redux-actions'; +import { call, put, takeLatest } from 'redux-saga/effects'; +import { + getNetworkEvents, + getNetworkEventsSuccess, + getNetworkEventsFail, + FetchNetworkEventsParams, +} from '../actions/network_events'; +import { fetchNetworkEvents } from '../api/network_events'; + +export function* fetchNetworkEventsEffect() { + yield takeLatest(getNetworkEvents, function* (action: Action) { + try { + const response = yield call(fetchNetworkEvents, action.payload); + + yield put( + getNetworkEventsSuccess({ + checkGroup: action.payload.checkGroup, + stepIndex: action.payload.stepIndex, + ...response, + }) + ); + } catch (e) { + yield put( + getNetworkEventsFail({ + checkGroup: action.payload.checkGroup, + stepIndex: action.payload.stepIndex, + error: e, + }) + ); + } + }); +} diff --git a/x-pack/plugins/uptime/public/state/reducers/index.ts b/x-pack/plugins/uptime/public/state/reducers/index.ts index c0bab124d5f9d..661b637802707 100644 --- a/x-pack/plugins/uptime/public/state/reducers/index.ts +++ b/x-pack/plugins/uptime/public/state/reducers/index.ts @@ -22,6 +22,7 @@ import { certificatesReducer } from '../certificates/certificates'; import { selectedFiltersReducer } from './selected_filters'; import { alertsReducer } from '../alerts/alerts'; import { journeyReducer } from './journey'; +import { networkEventsReducer } from './network_events'; export const rootReducer = combineReducers({ monitor: monitorReducer, @@ -41,4 +42,5 @@ export const rootReducer = combineReducers({ selectedFilters: selectedFiltersReducer, alerts: alertsReducer, journeys: journeyReducer, + networkEvents: networkEventsReducer, }); diff --git a/x-pack/plugins/uptime/public/state/reducers/journey.ts b/x-pack/plugins/uptime/public/state/reducers/journey.ts index e1c3dc808f1bf..133a5d1edb2c2 100644 --- a/x-pack/plugins/uptime/public/state/reducers/journey.ts +++ b/x-pack/plugins/uptime/public/state/reducers/journey.ts @@ -18,6 +18,7 @@ import { export interface JourneyState { checkGroup: string; steps: Ping[]; + details?: SyntheticsJourneyApiResponse['details']; loading: boolean; error?: Error; } @@ -56,13 +57,14 @@ export const journeyReducer = handleActions( [String(getJourneyStepsSuccess)]: ( state: JourneyKVP, - { payload: { checkGroup, steps } }: Action + { payload: { checkGroup, steps, details } }: Action ) => ({ ...state, [checkGroup]: { loading: false, checkGroup, steps, + details, }, }), diff --git a/x-pack/plugins/uptime/public/state/reducers/network_events.ts b/x-pack/plugins/uptime/public/state/reducers/network_events.ts new file mode 100644 index 0000000000000..44a23b0fa53d7 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/reducers/network_events.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { handleActions, Action } from 'redux-actions'; +import { NetworkEvent, SyntheticsNetworkEventsApiResponse } from '../../../common/runtime_types'; +import { + FetchNetworkEventsParams, + FetchNetworkEventsFailPayload, + getNetworkEvents, + getNetworkEventsFail, + getNetworkEventsSuccess, +} from '../actions/network_events'; + +export interface NetworkEventsState { + [checkGroup: string]: { + [stepIndex: number]: { + events: NetworkEvent[]; + loading: boolean; + error?: Error; + }; + }; +} + +const initialState: NetworkEventsState = {}; + +type Payload = FetchNetworkEventsParams & + SyntheticsNetworkEventsApiResponse & + FetchNetworkEventsFailPayload & + string[]; + +export const networkEventsReducer = handleActions( + { + [String(getNetworkEvents)]: ( + state: NetworkEventsState, + { payload: { checkGroup, stepIndex } }: Action + ) => ({ + ...state, + [checkGroup]: state[checkGroup] + ? { + [stepIndex]: state[checkGroup][stepIndex] + ? { + ...state[checkGroup][stepIndex], + loading: true, + events: [], + } + : { + loading: true, + events: [], + }, + } + : { + [stepIndex]: { + loading: true, + events: [], + }, + }, + }), + + [String(getNetworkEventsSuccess)]: ( + state: NetworkEventsState, + { + payload: { events, checkGroup, stepIndex }, + }: Action + ) => { + return { + ...state, + [checkGroup]: state[checkGroup] + ? { + [stepIndex]: state[checkGroup][stepIndex] + ? { + ...state[checkGroup][stepIndex], + loading: false, + events, + } + : { + loading: false, + events, + }, + } + : { + [stepIndex]: { + loading: false, + events, + }, + }, + }; + }, + + [String(getNetworkEventsFail)]: ( + state: NetworkEventsState, + { payload: { checkGroup, stepIndex, error } }: Action + ) => ({ + ...state, + [checkGroup]: state[checkGroup] + ? { + [stepIndex]: state[checkGroup][stepIndex] + ? { + ...state[checkGroup][stepIndex], + loading: false, + events: [], + error, + } + : { + loading: false, + events: [], + error, + }, + } + : { + [stepIndex]: { + loading: false, + events: [], + error, + }, + }, + }), + }, + initialState +); diff --git a/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts b/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts index f1a68318be863..64410b860b197 100644 --- a/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts +++ b/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts @@ -116,6 +116,7 @@ describe('state selectors', () => { anomalyAlertDeletion: { data: null, loading: false }, }, journeys: {}, + networkEvents: {}, }; it('selects base path from state', () => { diff --git a/x-pack/plugins/uptime/public/state/selectors/index.ts b/x-pack/plugins/uptime/public/state/selectors/index.ts index 6bfe67468aae5..eef53e1100029 100644 --- a/x-pack/plugins/uptime/public/state/selectors/index.ts +++ b/x-pack/plugins/uptime/public/state/selectors/index.ts @@ -96,3 +96,5 @@ export const selectedFiltersSelector = ({ selectedFilters }: AppState) => select export const monitorIdSelector = ({ ui: { monitorId } }: AppState) => monitorId; export const journeySelector = ({ journeys }: AppState) => journeys; + +export const networkEventsSelector = ({ networkEvents }: AppState) => networkEvents; diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_network_events.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_network_events.test.ts new file mode 100644 index 0000000000000..bb88911eedfb0 --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_network_events.test.ts @@ -0,0 +1,251 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getUptimeESMockClient } from './helper'; +import { getNetworkEvents } from '../get_network_events'; + +describe('getNetworkEvents', () => { + let mockHits: any; + + beforeEach(() => { + mockHits = [ + { + _index: 'heartbeat-2020.12.14', + _id: 'YMfcYHYBOm8nKLizQt1o', + _score: null, + _source: { + '@timestamp': '2020-12-14T10:46:39.183Z', + synthetics: { + step: { + name: 'Click next link', + index: 2, + }, + journey: { + name: 'inline', + id: 'inline', + }, + type: 'journey/network_info', + package_version: '0.0.1-alpha.8', + payload: { + load_end_time: 3287.298251, + response_received_time: 3287.299074, + method: 'GET', + step: { + index: 2, + name: 'Click next link', + }, + status: 200, + type: 'Image', + request_sent_time: 3287.154973, + url: 'www.test.com', + request: { + initial_priority: 'Low', + referrer_policy: 'no-referrer-when-downgrade', + url: 'www.test.com', + method: 'GET', + headers: { + referer: 'www.test.com', + user_agent: + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/88.0.4324.0 Safari/537.36', + }, + mixed_content_type: 'none', + }, + response: { + from_service_worker: false, + security_details: { + protocol: 'TLS 1.2', + key_exchange: 'ECDHE_RSA', + valid_to: 1638230399, + certificate_transparency_compliance: 'unknown', + cipher: 'AES_128_GCM', + issuer: 'DigiCert TLS RSA SHA256 2020 CA1', + subject_name: 'syndication.twitter.com', + valid_from: 1606694400, + signed_certificate_timestamp_list: [], + key_exchange_group: 'P-256', + san_list: [ + 'syndication.twitter.com', + 'syndication.twimg.com', + 'cdn.syndication.twitter.com', + 'cdn.syndication.twimg.com', + 'syndication-o.twitter.com', + 'syndication-o.twimg.com', + ], + certificate_id: 0, + }, + security_state: 'secure', + connection_reused: true, + remote_port: 443, + timing: { + ssl_start: -1, + send_start: 0.214, + ssl_end: -1, + connect_start: -1, + connect_end: -1, + send_end: 0.402, + dns_start: -1, + request_time: 3287.155502, + push_end: 0, + worker_fetch_start: -1, + worker_ready: -1, + worker_start: -1, + proxy_end: -1, + push_start: 0, + worker_respond_with_settled: -1, + proxy_start: -1, + dns_end: -1, + receive_headers_end: 142.215, + }, + connection_id: 852, + remote_i_p_address: '104.244.42.200', + encoded_data_length: 337, + response_time: 1.60794279932414e12, + from_prefetch_cache: false, + mime_type: 'image/gif', + from_disk_cache: false, + url: 'www.test.com', + protocol: 'h2', + headers: { + x_frame_options: 'SAMEORIGIN', + cache_control: 'no-cache, no-store, must-revalidate, pre-check=0, post-check=0', + strict_transport_security: 'max-age=631138519', + x_twitter_response_tags: 'BouncerCompliant', + content_type: 'image/gif;charset=utf-8', + expires: 'Tue, 31 Mar 1981 05:00:00 GMT', + date: 'Mon, 14 Dec 2020 10:46:39 GMT', + x_transaction: '008fff3d00a1e64c', + x_connection_hash: 'cb6fe99b8676f4e4b827cc3e6512c90d', + last_modified: 'Mon, 14 Dec 2020 10:46:39 GMT', + x_content_type_options: 'nosniff', + content_encoding: 'gzip', + x_xss_protection: '0', + server: 'tsa_f', + x_response_time: '108', + pragma: 'no-cache', + content_length: '65', + status: '200 OK', + }, + status_text: '', + status: 200, + }, + timings: { + proxy: -1, + connect: -1, + receive: 0.5340000002433953, + blocked: 0.21400000014182297, + ssl: -1, + send: 0.18799999998009298, + total: 143.27800000000934, + queueing: 0.5289999999149586, + wait: 141.81299999972907, + dns: -1, + }, + is_navigation_request: false, + timestamp: 1607942799183375, + }, + }, + }, + }, + ]; + }); + + it('Uses the correct query', async () => { + const { uptimeEsClient, esClient } = getUptimeESMockClient(); + + esClient.search.mockResolvedValueOnce({ + body: { + hits: { + hits: mockHits, + }, + }, + } as any); + + await getNetworkEvents({ + uptimeEsClient, + checkGroup: 'my-fake-group', + stepIndex: '1', + }); + + expect(esClient.search.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "synthetics.type": "journey/network_info", + }, + }, + Object { + "term": Object { + "monitor.check_group": "my-fake-group", + }, + }, + Object { + "term": Object { + "synthetics.step.index": 1, + }, + }, + ], + }, + }, + "size": 1000, + }, + "index": "heartbeat-8*", + }, + ], + ] + `); + }); + + it('Returns the correct result', async () => { + const { esClient, uptimeEsClient } = getUptimeESMockClient(); + + esClient.search.mockResolvedValueOnce({ + body: { + hits: { + hits: mockHits, + }, + }, + } as any); + + const result = await getNetworkEvents({ + uptimeEsClient, + checkGroup: 'my-fake-group', + stepIndex: '1', + }); + + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "loadEndTime": 3287298.251, + "method": "GET", + "mimeType": "image/gif", + "requestSentTime": 3287154.973, + "requestStartTime": 3287155.502, + "status": 200, + "timestamp": "2020-12-14T10:46:39.183Z", + "timings": Object { + "blocked": 0.21400000014182297, + "connect": -1, + "dns": -1, + "proxy": -1, + "queueing": 0.5289999999149586, + "receive": 0.5340000002433953, + "send": 0.18799999998009298, + "ssl": -1, + "total": 143.27800000000934, + "wait": 141.81299999972907, + }, + "url": "www.test.com", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts new file mode 100644 index 0000000000000..ef11b00604490 --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UMElasticsearchQueryFn } from '../adapters/framework'; +import { SyntheticsJourneyApiResponse } from '../../../common/runtime_types'; + +interface GetJourneyDetails { + checkGroup: string; +} + +export const getJourneyDetails: UMElasticsearchQueryFn< + GetJourneyDetails, + SyntheticsJourneyApiResponse['details'] +> = async ({ uptimeEsClient, checkGroup }) => { + const baseParams = { + query: { + bool: { + filter: [ + { + term: { + 'monitor.check_group': checkGroup, + }, + }, + { + term: { + 'synthetics.type': 'journey/end', + }, + }, + ], + }, + }, + _source: ['@timestamp', 'monitor.id'], + size: 1, + }; + + const { body: thisJourney } = await uptimeEsClient.search({ body: baseParams }); + + if (thisJourney?.hits?.hits.length > 0) { + const thisJourneySource: any = thisJourney.hits.hits[0]._source; + + const baseSiblingParams = { + query: { + bool: { + filter: [ + { + term: { + 'monitor.id': thisJourneySource.monitor.id, + }, + }, + { + term: { + 'synthetics.type': 'journey/end', + }, + }, + ], + }, + }, + _source: ['@timestamp', 'monitor.check_group'], + size: 1, + }; + + const previousParams = { + ...baseSiblingParams, + query: { + bool: { + filter: [ + ...baseSiblingParams.query.bool.filter, + { + range: { + '@timestamp': { + lt: thisJourneySource['@timestamp'], + }, + }, + }, + ], + }, + }, + sort: [{ '@timestamp': { order: 'desc' } }], + }; + + const nextParams = { + ...baseSiblingParams, + query: { + bool: { + filter: [ + ...baseSiblingParams.query.bool.filter, + { + range: { + '@timestamp': { + gt: thisJourneySource['@timestamp'], + }, + }, + }, + ], + }, + }, + sort: [{ '@timestamp': { order: 'asc' } }], + }; + + const { body: previousJourneyResult } = await uptimeEsClient.search({ body: previousParams }); + const { body: nextJourneyResult } = await uptimeEsClient.search({ body: nextParams }); + const previousJourney: any = + previousJourneyResult?.hits?.hits.length > 0 ? previousJourneyResult?.hits?.hits[0] : null; + const nextJourney: any = + nextJourneyResult?.hits?.hits.length > 0 ? nextJourneyResult?.hits?.hits[0] : null; + return { + timestamp: thisJourneySource['@timestamp'], + previous: previousJourney + ? { + checkGroup: previousJourney._source.monitor.check_group, + timestamp: previousJourney._source['@timestamp'], + } + : undefined, + next: nextJourney + ? { + checkGroup: nextJourney._source.monitor.check_group, + timestamp: nextJourney._source['@timestamp'], + } + : undefined, + } as SyntheticsJourneyApiResponse['details']; + } else { + return null; + } +}; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts new file mode 100644 index 0000000000000..1353175a8f94d --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UMElasticsearchQueryFn } from '../adapters/framework'; +import { NetworkEvent } from '../../../common/runtime_types'; + +interface GetNetworkEventsParams { + checkGroup: string; + stepIndex: string; +} + +export const getNetworkEvents: UMElasticsearchQueryFn< + GetNetworkEventsParams, + NetworkEvent[] +> = async ({ uptimeEsClient, checkGroup, stepIndex }) => { + const params = { + query: { + bool: { + filter: [ + { term: { 'synthetics.type': 'journey/network_info' } }, + { term: { 'monitor.check_group': checkGroup } }, + { term: { 'synthetics.step.index': Number(stepIndex) } }, + ], + }, + }, + // NOTE: This limit may need tweaking in the future. Users can technically perform multiple + // navigations within one step, and may push up against this limit, however this manner + // of usage isn't advised. + size: 1000, + }; + + const { body: result } = await uptimeEsClient.search({ body: params }); + + const microToMillis = (micro: number): number => (micro === -1 ? -1 : micro * 1000); + + return result.hits.hits.map((event: any) => { + const requestSentTime = microToMillis(event._source.synthetics.payload.request_sent_time); + const loadEndTime = microToMillis(event._source.synthetics.payload.load_end_time); + const requestStartTime = + event._source.synthetics.payload.response && event._source.synthetics.payload.response.timing + ? microToMillis(event._source.synthetics.payload.response.timing.request_time) + : undefined; + + return { + timestamp: event._source['@timestamp'], + method: event._source.synthetics.payload?.method, + url: event._source.synthetics.payload?.url, + status: event._source.synthetics.payload?.status, + mimeType: event._source.synthetics.payload?.response?.mime_type, + requestSentTime, + requestStartTime, + loadEndTime, + timings: event._source.synthetics.payload.timings, + }; + }); +}; diff --git a/x-pack/plugins/uptime/server/lib/requests/index.ts b/x-pack/plugins/uptime/server/lib/requests/index.ts index fd7e5f6041719..34137fe400b00 100644 --- a/x-pack/plugins/uptime/server/lib/requests/index.ts +++ b/x-pack/plugins/uptime/server/lib/requests/index.ts @@ -20,6 +20,8 @@ import { getSnapshotCount } from './get_snapshot_counts'; import { getIndexStatus } from './get_index_status'; import { getJourneySteps } from './get_journey_steps'; import { getJourneyScreenshot } from './get_journey_screenshot'; +import { getJourneyDetails } from './get_journey_details'; +import { getNetworkEvents } from './get_network_events'; import { getJourneyFailedSteps } from './get_journey_failed_steps'; export const requests = { @@ -40,6 +42,8 @@ export const requests = { getJourneySteps, getJourneyFailedSteps, getJourneyScreenshot, + getJourneyDetails, + getNetworkEvents, }; export type UptimeRequests = typeof requests; diff --git a/x-pack/plugins/uptime/server/rest_api/index.ts b/x-pack/plugins/uptime/server/rest_api/index.ts index a2475792edfbe..4db2da541079c 100644 --- a/x-pack/plugins/uptime/server/rest_api/index.ts +++ b/x-pack/plugins/uptime/server/rest_api/index.ts @@ -24,6 +24,7 @@ import { } from './monitors'; import { createGetMonitorDurationRoute } from './monitors/monitors_durations'; import { createGetIndexPatternRoute, createGetIndexStatusRoute } from './index_state'; +import { createNetworkEventsRoute } from './network_events'; import { createJourneyFailedStepsRoute } from './pings/journeys'; export * from './types'; @@ -48,5 +49,6 @@ export const restApiRoutes: UMRestApiRouteFactory[] = [ createGetMonitorDurationRoute, createJourneyRoute, createJourneyScreenshotRoute, + createNetworkEventsRoute, createJourneyFailedStepsRoute, ]; diff --git a/x-pack/plugins/uptime/server/rest_api/network_events/get_network_events.ts b/x-pack/plugins/uptime/server/rest_api/network_events/get_network_events.ts new file mode 100644 index 0000000000000..f24b319baff00 --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/network_events/get_network_events.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { UMServerLibs } from '../../lib/lib'; +import { UMRestApiRouteFactory } from '../types'; + +export const createNetworkEventsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ + method: 'GET', + path: '/api/uptime/network_events', + validate: { + query: schema.object({ + checkGroup: schema.string(), + stepIndex: schema.number(), + }), + }, + handler: async ({ uptimeEsClient, request }): Promise => { + const { checkGroup, stepIndex } = request.query; + + const result = await libs.requests.getNetworkEvents({ + uptimeEsClient, + checkGroup, + stepIndex, + }); + + return { + events: result, + }; + }, +}); diff --git a/x-pack/plugins/uptime/server/rest_api/network_events/index.ts b/x-pack/plugins/uptime/server/rest_api/network_events/index.ts new file mode 100644 index 0000000000000..3f3c1afe06f99 --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/network_events/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createNetworkEventsRoute } from './get_network_events'; diff --git a/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts b/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts index 8ebd4b4609c75..b2559ee8d7054 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts @@ -24,9 +24,15 @@ export const createJourneyRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => checkGroup, }); + const details = await libs.requests.getJourneyDetails({ + uptimeEsClient, + checkGroup, + }); + return { checkGroup, steps: result, + details, }; }, }); From 4d398f24613c0ec9c88677dc97e184441f7a1d17 Mon Sep 17 00:00:00 2001 From: Constance Date: Mon, 14 Dec 2020 13:02:49 -0800 Subject: [PATCH 20/44] [App Search] Temporarily remove sidebar layout and internal engine links for 7.11 release (#85820) * Hide layout/sidebar for main Engines Overview * Remove internal links to Engine pages Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/engines/engines_table.test.tsx | 7 ++- .../components/engines/engines_table.tsx | 15 +++--- .../applications/app_search/index.test.tsx | 2 +- .../public/applications/app_search/index.tsx | 50 ++++++++++--------- 4 files changed, 39 insertions(+), 35 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx index ea7eeea750cc4..a30b5c6858f7c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx @@ -9,8 +9,7 @@ import '../../../__mocks__/enterprise_search_url.mock'; import { mockTelemetryActions, mountWithIntl } from '../../../__mocks__/'; import React from 'react'; -import { EuiBasicTable, EuiPagination, EuiButtonEmpty } from '@elastic/eui'; -import { EuiLinkTo } from '../../../shared/react_router_helpers'; +import { EuiBasicTable, EuiPagination, EuiButtonEmpty, EuiLink } from '@elastic/eui'; import { EnginesTable } from './engines_table'; @@ -55,10 +54,10 @@ describe('EnginesTable', () => { }); it('contains engine links which send telemetry', () => { - const engineLinks = wrapper.find(EuiLinkTo); + const engineLinks = wrapper.find(EuiLink); engineLinks.forEach((link) => { - expect(link.prop('to')).toEqual('/engines/test-engine'); + expect(link.prop('href')).toEqual('http://localhost:3002/as/engines/test-engine'); link.simulate('click'); expect(mockTelemetryActions.sendAppSearchTelemetry).toHaveBeenCalledWith({ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx index e9805ab8f2711..58922e439fc76 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx @@ -6,12 +6,12 @@ import React from 'react'; import { useActions } from 'kea'; -import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; +import { EuiBasicTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui'; import { FormattedMessage, FormattedDate, FormattedNumber } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { TelemetryLogic } from '../../../shared/telemetry'; -import { EuiLinkTo } from '../../../shared/react_router_helpers'; +import { getAppSearchUrl } from '../../../shared/enterprise_search_url'; import { getEngineRoute } from '../../routes'; import { ENGINES_PAGE_SIZE } from '../../../../../common/constants'; @@ -47,7 +47,8 @@ export const EnginesTable: React.FC = ({ const { sendAppSearchTelemetry } = useActions(TelemetryLogic); const engineLinkProps = (name: string) => ({ - to: getEngineRoute(name), + href: getAppSearchUrl(getEngineRoute(name)), + target: '_blank', onClick: () => sendAppSearchTelemetry({ action: 'clicked', @@ -62,9 +63,9 @@ export const EnginesTable: React.FC = ({ defaultMessage: 'Name', }), render: (name: string) => ( - + {name} - + ), width: '30%', truncateText: true, @@ -137,12 +138,12 @@ export const EnginesTable: React.FC = ({ ), dataType: 'string', render: (name: string) => ( - + - + ), align: 'right', width: '100px', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 11387734e9f9e..ce1e82ab2d57e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -55,7 +55,7 @@ describe('AppSearchConfigured', () => { it('renders with layout', () => { const wrapper = shallow(); - expect(wrapper.find(Layout)).toHaveLength(2); + expect(wrapper.find(Layout)).toHaveLength(1); expect(wrapper.find(Layout).last().prop('readOnlyMode')).toBeFalsy(); expect(wrapper.find(EnginesOverview)).toHaveLength(1); expect(wrapper.find(EngineRouter)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 769230ccffd22..efa95d2033c10 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -8,6 +8,8 @@ import React, { useEffect } from 'react'; import { Route, Redirect, Switch } from 'react-router-dom'; import { useActions, useValues } from 'kea'; +import { EuiPage, EuiPageBody } from '@elastic/eui'; + import { getAppSearchUrl } from '../shared/enterprise_search_url'; import { KibanaLogic } from '../shared/kibana'; import { HttpLogic } from '../shared/http'; @@ -79,29 +81,31 @@ export const AppSearchConfigured: React.FC = (props) => { - } readOnlyMode={readOnlyMode}> - {errorConnecting ? ( - - ) : ( - - - - - - - - - - - - - - - - - - )} - + + + {errorConnecting ? ( + + ) : ( + + + + + + + + + + + + + + + + + + )} + + ); From 23c5daa6229c0a7451f3843b683610522e3a5a45 Mon Sep 17 00:00:00 2001 From: nnamdifrankie <56440728+nnamdifrankie@users.noreply.github.com> Date: Mon, 14 Dec 2020 16:15:55 -0500 Subject: [PATCH 21/44] [Fleet] add ilm policy per data stream (#85492) Co-authored-by: kevinlog Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/fleet/common/types/models/epm.ts | 2 + .../fleet/sections/epm/constants.tsx | 1 + .../elasticsearch/datastream_ilm/install.ts | 137 ++++++++++++++++++ .../elasticsearch/datastream_ilm/remove.ts | 42 ++++++ .../epm/elasticsearch/template/install.ts | 1 + .../epm/elasticsearch/template/template.ts | 6 +- .../services/epm/packages/_install_package.ts | 9 ++ .../server/services/epm/packages/remove.ts | 3 + .../apis/epm/install_remove_assets.ts | 9 ++ .../apis/epm/update_assets.ts | 4 + .../elasticsearch/ilm_policy/all_assets.json | 15 ++ 11 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/install.ts create mode 100644 x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/remove.ts create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/data_stream/test_metrics/elasticsearch/ilm_policy/all_assets.json diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index f518c606d6959..77625e48dbc96 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -65,6 +65,7 @@ export enum ElasticsearchAssetType { indexTemplate = 'index_template', ilmPolicy = 'ilm_policy', transform = 'transform', + dataStreamIlmPolicy = 'data_stream_ilm_policy', } export type DataType = typeof dataTypes; @@ -207,6 +208,7 @@ export type ElasticsearchAssetTypeToParts = Record< export interface RegistryDataStream { type: string; + ilm_policy?: string; hidden?: boolean; dataset: string; title: string; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx index fe5390e75f6a1..26e36621802fd 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx @@ -27,6 +27,7 @@ export const AssetTitleMap: Record = { visualization: 'Visualization', input: 'Agent input', map: 'Map', + data_stream_ilm_policy: 'Data Stream ILM Policy', }; export const ServiceTitleMap: Record, string> = { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/install.ts new file mode 100644 index 0000000000000..6b5950416af56 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/install.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; +import { + ElasticsearchAssetType, + EsAssetReference, + InstallablePackage, + RegistryDataStream, +} from '../../../../../common/types/models'; +import { CallESAsCurrentUser } from '../../../../types'; +import { getInstallation } from '../../packages'; +import { deleteIlmRefs, deleteIlms } from './remove'; +import { saveInstalledEsRefs } from '../../packages/install'; +import { getAsset } from '../transform/common'; + +interface IlmInstallation { + installationName: string; + content: string; +} + +interface IlmPathDataset { + path: string; + dataStream: RegistryDataStream; +} + +export const installIlmForDataStream = async ( + registryPackage: InstallablePackage, + paths: string[], + callCluster: CallESAsCurrentUser, + savedObjectsClient: SavedObjectsClientContract +) => { + const installation = await getInstallation({ savedObjectsClient, pkgName: registryPackage.name }); + let previousInstalledIlmEsAssets: EsAssetReference[] = []; + if (installation) { + previousInstalledIlmEsAssets = installation.installed_es.filter( + ({ type, id }) => type === ElasticsearchAssetType.dataStreamIlmPolicy + ); + } + + // delete all previous ilm + await deleteIlms( + callCluster, + previousInstalledIlmEsAssets.map((asset) => asset.id) + ); + // install the latest dataset + const dataStreams = registryPackage.data_streams; + if (!dataStreams?.length) return []; + const dataStreamIlmPaths = paths.filter((path) => isDataStreamIlm(path)); + let installedIlms: EsAssetReference[] = []; + if (dataStreamIlmPaths.length > 0) { + const ilmPathDatasets = dataStreams.reduce((acc, dataStream) => { + dataStreamIlmPaths.forEach((path) => { + if (isDatasetIlm(path, dataStream.path)) { + acc.push({ path, dataStream }); + } + }); + return acc; + }, []); + + const ilmRefs = ilmPathDatasets.reduce((acc, ilmPathDataset) => { + if (ilmPathDataset) { + acc.push({ + id: getIlmNameForInstallation(ilmPathDataset), + type: ElasticsearchAssetType.dataStreamIlmPolicy, + }); + } + return acc; + }, []); + + await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, ilmRefs); + + const ilmInstallations: IlmInstallation[] = ilmPathDatasets.map( + (ilmPathDataset: IlmPathDataset) => { + return { + installationName: getIlmNameForInstallation(ilmPathDataset), + content: getAsset(ilmPathDataset.path).toString('utf-8'), + }; + } + ); + + const installationPromises = ilmInstallations.map(async (ilmInstallation) => { + return handleIlmInstall({ callCluster, ilmInstallation }); + }); + + installedIlms = await Promise.all(installationPromises).then((results) => results.flat()); + } + + if (previousInstalledIlmEsAssets.length > 0) { + const currentInstallation = await getInstallation({ + savedObjectsClient, + pkgName: registryPackage.name, + }); + + // remove the saved object reference + await deleteIlmRefs( + savedObjectsClient, + currentInstallation?.installed_es || [], + registryPackage.name, + previousInstalledIlmEsAssets.map((asset) => asset.id), + installedIlms.map((installed) => installed.id) + ); + } + return installedIlms; +}; + +async function handleIlmInstall({ + callCluster, + ilmInstallation, +}: { + callCluster: CallESAsCurrentUser; + ilmInstallation: IlmInstallation; +}): Promise { + await callCluster('transport.request', { + method: 'PUT', + path: `/_ilm/policy/${ilmInstallation.installationName}`, + body: ilmInstallation.content, + }); + + return { id: ilmInstallation.installationName, type: ElasticsearchAssetType.dataStreamIlmPolicy }; +} + +const isDataStreamIlm = (path: string) => { + return new RegExp('(?.*)/data_stream/(?.*)/elasticsearch/ilm/*.*').test(path); +}; + +const isDatasetIlm = (path: string, datasetName: string) => { + return new RegExp(`(?.*)/data_stream\\/${datasetName}/elasticsearch/ilm/*.*`).test(path); +}; + +const getIlmNameForInstallation = (ilmPathDataset: IlmPathDataset) => { + const filename = ilmPathDataset?.path.split('/')?.pop()?.split('.')[0]; + return `${ilmPathDataset.dataStream.type}-${ilmPathDataset.dataStream.package}.${ilmPathDataset.dataStream.path}-${filename}`; +}; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/remove.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/remove.ts new file mode 100644 index 0000000000000..f36599365734c --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/remove.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; +import { CallESAsCurrentUser, ElasticsearchAssetType, EsAssetReference } from '../../../../types'; +import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common/constants'; + +export const deleteIlms = async (callCluster: CallESAsCurrentUser, ilmPolicyIds: string[]) => { + await Promise.all( + ilmPolicyIds.map(async (ilmPolicyId) => { + await callCluster('transport.request', { + method: 'DELETE', + path: `_ilm/policy/${ilmPolicyId}`, + ignore: [404, 400], + }); + }) + ); +}; + +export const deleteIlmRefs = async ( + savedObjectsClient: SavedObjectsClientContract, + installedEsAssets: EsAssetReference[], + pkgName: string, + installedEsIdToRemove: string[], + currentInstalledEsIlmIds: string[] +) => { + const seen = new Set(); + const filteredAssets = installedEsAssets.filter(({ type, id }) => { + if (type !== ElasticsearchAssetType.dataStreamIlmPolicy) return true; + const add = + (currentInstalledEsIlmIds.includes(id) || !installedEsIdToRemove.includes(id)) && + !seen.has(id); + seen.add(id); + return add; + }); + return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + installed_es: filteredAssets, + }); +}; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index 944f742e54546..8b018f4a2a906 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -314,6 +314,7 @@ export async function installTemplate({ pipelineName, packageName, composedOfTemplates, + ilmPolicy: dataStream.ilm_policy, hidden: dataStream.hidden, }); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts index d80d54d098db7..fd75139d4cd45 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts @@ -45,6 +45,7 @@ export function getTemplate({ pipelineName, packageName, composedOfTemplates, + ilmPolicy, hidden, }: { type: string; @@ -53,6 +54,7 @@ export function getTemplate({ pipelineName?: string | undefined; packageName: string; composedOfTemplates: string[]; + ilmPolicy?: string | undefined; hidden?: boolean; }): IndexTemplate { const template = getBaseTemplate( @@ -61,6 +63,7 @@ export function getTemplate({ mappings, packageName, composedOfTemplates, + ilmPolicy, hidden ); if (pipelineName) { @@ -263,6 +266,7 @@ function getBaseTemplate( mappings: IndexTemplateMappings, packageName: string, composedOfTemplates: string[], + ilmPolicy?: string | undefined, hidden?: boolean ): IndexTemplate { // Meta information to identify Ingest Manager's managed templates and indices @@ -287,7 +291,7 @@ function getBaseTemplate( index: { // ILM Policy must be added here, for now point to the default global ILM policy name lifecycle: { - name: type, + name: ilmPolicy ? ilmPolicy : type, }, // What should be our default for the compression? codec: 'best_compression', diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index 5e6ecad9b72f1..c0e2fcb12bcf8 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -29,6 +29,7 @@ import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; import { deleteKibanaSavedObjectsAssets } from './remove'; import { installTransform } from '../elasticsearch/transform/install'; import { createInstallation, saveKibanaAssetsRefs, updateVersion } from './install'; +import { installIlmForDataStream } from '../elasticsearch/datastream_ilm/install'; import { saveArchiveEntries } from '../archive/storage'; import { ConcurrentInstallOperationError } from '../../../errors'; @@ -134,6 +135,13 @@ export async function _installPackage({ // per data stream and we should then save them await installILMPolicy(paths, callCluster); + const installedDataStreamIlm = await installIlmForDataStream( + packageInfo, + paths, + callCluster, + savedObjectsClient + ); + // installs versionized pipelines without removing currently installed ones const installedPipelines = await installPipelines( packageInfo, @@ -212,6 +220,7 @@ export async function _installPackage({ return [ ...installedKibanaAssetsRefs, ...installedPipelines, + ...installedDataStreamIlm, ...installedTemplateRefs, ...installedTransforms, ]; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index 63bf1ed53fb97..331b6bfa882da 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -23,6 +23,7 @@ import { deleteTransforms } from '../elasticsearch/transform/remove'; import { packagePolicyService, appContextService } from '../..'; import { splitPkgKey } from '../registry'; import { deletePackageCache } from '../archive'; +import { deleteIlms } from '../elasticsearch/datastream_ilm/remove'; import { removeArchiveEntries } from '../archive/storage'; export async function removeInstallation(options: { @@ -93,6 +94,8 @@ function deleteESAssets(installedObjects: EsAssetReference[], callCluster: CallE return deleteTemplate(callCluster, id); } else if (assetType === ElasticsearchAssetType.transform) { return deleteTransforms(callCluster, [id]); + } else if (assetType === ElasticsearchAssetType.dataStreamIlmPolicy) { + return deleteIlms(callCluster, [id]); } }); } diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts index a7d46b9c6677e..1d5f864c27eea 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts @@ -459,6 +459,14 @@ const expectAssetsInstalled = ({ }, ], installed_es: [ + { + id: 'logs-all_assets.test_logs-all_assets', + type: 'data_stream_ilm_policy', + }, + { + id: 'metrics-all_assets.test_metrics-all_assets', + type: 'data_stream_ilm_policy', + }, { id: 'logs-all_assets.test_logs', type: 'index_template', @@ -496,6 +504,7 @@ const expectAssetsInstalled = ({ { id: '96c6eb85-fe2e-56c6-84be-5fda976796db', type: 'epm-packages-assets' }, { id: '2d73a161-fa69-52d0-aa09-1bdc691b95bb', type: 'epm-packages-assets' }, { id: '0a00c2d2-ce63-5b9c-9aa0-0cf1938f7362', type: 'epm-packages-assets' }, + { id: '691f0505-18c5-57a6-9f40-06e8affbdf7a', type: 'epm-packages-assets' }, { id: 'b36e6dd0-58f7-5dd0-a286-8187e4019274', type: 'epm-packages-assets' }, { id: 'f839c76e-d194-555a-90a1-3265a45789e4', type: 'epm-packages-assets' }, { id: '9af7bbb3-7d8a-50fa-acc9-9dde6f5efca2', type: 'epm-packages-assets' }, diff --git a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts index 37aa94beec8b0..7b264f949532e 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts @@ -293,6 +293,10 @@ export default function (providerContext: FtrProviderContext) { }, ], installed_es: [ + { + id: 'logs-all_assets.test_logs-all_assets', + type: 'data_stream_ilm_policy', + }, { id: 'logs-all_assets.test_logs-0.2.0', type: 'ingest_pipeline', diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/data_stream/test_metrics/elasticsearch/ilm_policy/all_assets.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/data_stream/test_metrics/elasticsearch/ilm_policy/all_assets.json new file mode 100644 index 0000000000000..7cf62e890f865 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/data_stream/test_metrics/elasticsearch/ilm_policy/all_assets.json @@ -0,0 +1,15 @@ +{ + "policy": { + "phases": { + "hot": { + "min_age": "0ms", + "actions": { + "rollover": { + "max_size": "50gb", + "max_age": "30d" + } + } + } + } + } +} \ No newline at end of file From 1f774bb2e62c2cde8840d29aa595e1c0889700d5 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Mon, 14 Dec 2020 16:38:05 -0500 Subject: [PATCH 22/44] [task manager] provide warning when setting max_workers greater than limit (#85574) resolves https://github.com/elastic/kibana/issues/56573 In this PR we create a new task manager limit on the config property `xpack.task_manager.max_workers` of 100, but only log a deprecation warning if that property exceeds the limit. We'll enforce the limit in 8.0. The rationale is that it's unlikely going to be useful to run with more than some number of workers, due to the amount of simultaneous work that would end up happening. In practice, too many workers can slow things down more than speed them up. We're setting the limit to 100 for now, but may increase / decrease it based on further research. --- x-pack/plugins/task_manager/server/config.ts | 1 + x-pack/plugins/task_manager/server/index.test.ts | 9 +++++++++ x-pack/plugins/task_manager/server/index.ts | 7 ++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/task_manager/server/config.ts b/x-pack/plugins/task_manager/server/config.ts index 157f01281836d..a22c4484389ae 100644 --- a/x-pack/plugins/task_manager/server/config.ts +++ b/x-pack/plugins/task_manager/server/config.ts @@ -6,6 +6,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; +export const MAX_WORKERS_LIMIT = 100; export const DEFAULT_MAX_WORKERS = 10; export const DEFAULT_POLL_INTERVAL = 3000; export const DEFAULT_MAX_POLL_INACTIVITY_CYCLES = 10; diff --git a/x-pack/plugins/task_manager/server/index.test.ts b/x-pack/plugins/task_manager/server/index.test.ts index 3f25f4403d358..873950f229147 100644 --- a/x-pack/plugins/task_manager/server/index.test.ts +++ b/x-pack/plugins/task_manager/server/index.test.ts @@ -40,4 +40,13 @@ describe('deprecations', () => { `); }); }); + + it('logs a warning if max_workers is over limit', () => { + const { messages } = applyTaskManagerDeprecations({ max_workers: 1000 }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "setting \\"xpack.task_manager.max_workers\\" (1000) greater than 100 is deprecated. Values greater than 100 will not be supported starting in 8.0.", + ] + `); + }); }); diff --git a/x-pack/plugins/task_manager/server/index.ts b/x-pack/plugins/task_manager/server/index.ts index 8f96e10430b39..1696a3ec69c05 100644 --- a/x-pack/plugins/task_manager/server/index.ts +++ b/x-pack/plugins/task_manager/server/index.ts @@ -7,7 +7,7 @@ import { get } from 'lodash'; import { PluginConfigDescriptor, PluginInitializerContext } from 'src/core/server'; import { TaskManagerPlugin } from './plugin'; -import { configSchema, TaskManagerConfig } from './config'; +import { configSchema, TaskManagerConfig, MAX_WORKERS_LIMIT } from './config'; export const plugin = (initContext: PluginInitializerContext) => new TaskManagerPlugin(initContext); @@ -37,6 +37,11 @@ export const config: PluginConfigDescriptor = { `"${fromPath}.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details` ); } + if (taskManager?.max_workers > MAX_WORKERS_LIMIT) { + log( + `setting "${fromPath}.max_workers" (${taskManager?.max_workers}) greater than ${MAX_WORKERS_LIMIT} is deprecated. Values greater than ${MAX_WORKERS_LIMIT} will not be supported starting in 8.0.` + ); + } return settings; }, ], From 6bf83fc37a30cc7595687f797115ca62d83e31f4 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Mon, 14 Dec 2020 16:04:24 -0600 Subject: [PATCH 23/44] skip 'should show failed shards popup' #78743 --- x-pack/test/functional/apps/discover/async_scripted_fields.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/discover/async_scripted_fields.js b/x-pack/test/functional/apps/discover/async_scripted_fields.js index 33a64e4f9cdd3..247c9c81015d3 100644 --- a/x-pack/test/functional/apps/discover/async_scripted_fields.js +++ b/x-pack/test/functional/apps/discover/async_scripted_fields.js @@ -40,7 +40,7 @@ export default function ({ getService, getPageObjects }) { await security.testUser.restoreDefaults(); }); - it('query should show failed shards pop up', async function () { + it.skip('query should show failed shards pop up', async function () { if (false) { /* If you had to modify the scripted fields, you could un-comment all this, run it, use es_archiver to update 'kibana_scripted_fields_on_logstash' */ From 504c8739de1cc9a597987df8e466a45ba290f282 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 14 Dec 2020 14:07:50 -0800 Subject: [PATCH 24/44] test:jest improvements to better support our monorepo (#84848) Signed-off-by: Tyler Smalley --- .ci/teamcity/default/jest.sh | 4 +- .ci/teamcity/oss/jest.sh | 2 +- .ci/teamcity/oss/jest_integration.sh | 2 +- .../development-functional-tests.asciidoc | 2 +- .../contributing/development-tests.asciidoc | 106 ++++++++++------- package.json | 2 - packages/kbn-test/src/index.ts | 2 + packages/kbn-test/src/jest/run.test.ts | 38 ++++++ packages/kbn-test/src/jest/run.ts | 110 ++++++++++++++++++ scripts/jest.js | 25 +--- scripts/jest_integration.js | 25 +--- .../server/saved_objects/migrations/README.md | 8 +- src/plugins/embeddable/README.asciidoc | 2 +- test/scripts/jenkins_unit.sh | 2 +- test/scripts/jenkins_xpack.sh | 18 +-- test/scripts/test/jest_integration.sh | 2 +- test/scripts/test/jest_unit.sh | 2 +- test/scripts/test/xpack_jest_unit.sh | 4 +- x-pack/README.md | 39 +------ x-pack/package.json | 2 +- x-pack/plugins/beats_management/readme.md | 8 +- .../plugins/encrypted_saved_objects/README.md | 9 +- x-pack/plugins/enterprise_search/README.md | 6 +- x-pack/plugins/event_log/README.md | 7 +- x-pack/plugins/graph/README.md | 2 +- x-pack/plugins/lens/readme.md | 2 +- x-pack/plugins/maps/README.md | 2 +- x-pack/plugins/ml/readme.md | 12 +- x-pack/plugins/task_manager/README.md | 6 +- x-pack/plugins/transform/readme.md | 12 +- x-pack/plugins/uptime/README.md | 6 +- x-pack/scripts/jest.js | 13 +-- 32 files changed, 285 insertions(+), 197 deletions(-) create mode 100644 packages/kbn-test/src/jest/run.test.ts create mode 100644 packages/kbn-test/src/jest/run.ts diff --git a/.ci/teamcity/default/jest.sh b/.ci/teamcity/default/jest.sh index 93ca7f76f3a21..b900d1b6d6b4e 100755 --- a/.ci/teamcity/default/jest.sh +++ b/.ci/teamcity/default/jest.sh @@ -6,7 +6,5 @@ source "$(dirname "${0}")/../util.sh" export JOB=kibana-default-jest -cd "$XPACK_DIR" - checks-reporter-with-killswitch "Jest Unit Tests" \ - node scripts/jest --bail --debug + node scripts/jest x-pack --ci --verbose --maxWorkers=5 diff --git a/.ci/teamcity/oss/jest.sh b/.ci/teamcity/oss/jest.sh index 3ba9ab0c31c57..0dee07d00d2be 100755 --- a/.ci/teamcity/oss/jest.sh +++ b/.ci/teamcity/oss/jest.sh @@ -7,4 +7,4 @@ source "$(dirname "${0}")/../util.sh" export JOB=kibana-oss-jest checks-reporter-with-killswitch "OSS Jest Unit Tests" \ - node scripts/jest --ci --verbose + node scripts/jest --config jest.config.oss.js --ci --verbose --maxWorkers=5 diff --git a/.ci/teamcity/oss/jest_integration.sh b/.ci/teamcity/oss/jest_integration.sh index 1a23c46c8a2c2..4c51d2ff29888 100755 --- a/.ci/teamcity/oss/jest_integration.sh +++ b/.ci/teamcity/oss/jest_integration.sh @@ -7,4 +7,4 @@ source "$(dirname "${0}")/../util.sh" export JOB=kibana-oss-jest-integration checks-reporter-with-killswitch "OSS Jest Integration Tests" \ - node scripts/jest_integration --verbose + node scripts/jest_integration --ci --verbose diff --git a/docs/developer/contributing/development-functional-tests.asciidoc b/docs/developer/contributing/development-functional-tests.asciidoc index 580a5a000f391..f149e9de7aaba 100644 --- a/docs/developer/contributing/development-functional-tests.asciidoc +++ b/docs/developer/contributing/development-functional-tests.asciidoc @@ -6,7 +6,7 @@ We use functional tests to make sure the {kib} UI works as expected. It replaces [discrete] === Running functional tests -The `FunctionalTestRunner` is very bare bones and gets most of its functionality from its config file, located at {blob}test/functional/config.js[test/functional/config.js]. If you’re writing a plugin outside the {kib} repo, you will have your own config file. +The `FunctionalTestRunner` is very bare bones and gets most of its functionality from its config file, located at {blob}test/functional/config.js[test/functional/config.js] or {blob}x-pack/test/functional/config.js[x-pack/test/functional/config.js]. If you’re writing a plugin outside the {kib} repo, you will have your own config file. See <> for more info. There are three ways to run the tests depending on your goals: diff --git a/docs/developer/contributing/development-tests.asciidoc b/docs/developer/contributing/development-tests.asciidoc index 4cf667195153d..647dc8b3f3b26 100644 --- a/docs/developer/contributing/development-tests.asciidoc +++ b/docs/developer/contributing/development-tests.asciidoc @@ -1,8 +1,6 @@ [[development-tests]] == Testing -To ensure that your changes will not break other functionality, please run the test suite and build (<>) before submitting your Pull Request. - [discrete] === Running specific {kib} tests @@ -13,63 +11,57 @@ invoke them: |=== |Test runner |Test location |Runner command (working directory is {kib} root) -|Jest |`src/**/*.test.js` `src/**/*.test.ts` -|`yarn test:jest -t regexp [test path]` +|Jest |`**/*.test.{js,mjs,ts,tsx}` +|`yarn test:jest [test path]` -|Jest (integration) |`**/integration_tests/**/*.test.js` -|`yarn test:jest_integration -t regexp [test path]` +|Jest (integration) |`**/integration_tests/**/*.test.{js,mjs,ts,tsx}` +|`yarn test:jest_integration [test path]` |Mocha -|`src/**/__tests__/**/*.js` `!src/**/public/__tests__/*.js` `packages/kbn-dev-utils/src/**/__tests__/**/*.js` `tasks/**/__tests__/**/*.js` +|`**/__tests__/**/*.js` |`node scripts/mocha --grep=regexp [test path]` |Functional -|`test/*integration/**/config.js` `test/*functional/**/config.js` `test/accessibility/config.js` -|`yarn test:ftr:server --config test/[directory]/config.js``yarn test:ftr:runner --config test/[directory]/config.js --grep=regexp` +|`test/**/config.js` `x-pack/test/**/config.js` +|`node scripts/functional_tests_server --config [directory]/config.js``node scripts/functional_test_runner_ --config [directory]/config.js --grep=regexp` |=== -For X-Pack tests located in `x-pack/` see -link:{kib-repo}tree/{branch}/x-pack/README.md#testing[X-Pack Testing] - Test runner arguments: - Where applicable, the optional arguments -`-t=regexp` or `--grep=regexp` will only run tests or test suites +`--grep=regexp` will only run tests or test suites whose descriptions matches the regular expression. - `[test path]` is the relative path to the test file. -Examples: - Run the entire elasticsearch_service test suite: -`yarn test:jest src/core/server/elasticsearch/elasticsearch_service.test.ts` -- Run the jest test case whose description matches -`stops both admin and data clients`: -`yarn test:jest -t 'stops both admin and data clients' src/core/server/elasticsearch/elasticsearch_service.test.ts` -- Run the api integration test case whose description matches the given -string: ``` yarn test:ftr:server –config test/api_integration/config.js -yarn test:ftr:runner –config test/api_integration/config +=== Unit Testing -[discrete] -=== Cross-browser compatibility +Kibana primarily uses Jest for unit testing. Each plugin or package defines a `jest.config.js` that extends link:{kib-repo}tree/{branch}/packages/kbn-test/jest-preset.js[a preset] provided by the link:{kib-repo}tree/{branch}/packages/kbn-test[`@kbn/test`] package. Unless you intend to run all unit tests within the project, it's most efficient to provide the Jest configuration file for the plugin or package you're testing. -**Testing IE on OS X** +[source,bash] +---- +yarn jest --config src/plugins/dashboard/jest.config.js +---- -**Note:** IE11 is not supported from 7.9 onwards. +A script is available to provide a better user experience when testing while navigating throughout the repository. To run the tests within your current working directory, use `yarn test:jest`. Like the Jest CLI, you can also supply a path to determine which tests to run. + +[source,bash] +---- +kibana/src/plugins/dashboard/server$ yarn test:jest #or +kibana/src/plugins/dashboard$ yarn test:jest server #or +kibana$ yarn test:jest src/plugins/dashboard/server +---- + +Any additional options supplied to `test:jest` will be passed onto the Jest CLI with the resulting Jest command always being outputted. + +[source,bash] +---- +kibana/src/plugins/dashboard/server$ yarn test:jest --coverage + +# is equivelant to + +yarn jest --coverage --verbose --config /home/tyler/elastic/kibana/src/plugins/dashboard/jest.config.js server +---- + +NOTE: There are still a handful of legacy tests that use the Mocha test runner. For those tests, use `node scripts/mocha --grep=regexp [test path]`. Tests using Mocha are located within `__tests__` directories. -* http://www.vmware.com/products/fusion/fusion-evaluation.html[Download -VMWare Fusion]. -* https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/#downloads[Download -IE virtual machines] for VMWare. -* Open VMWare and go to Window > Virtual Machine Library. Unzip the -virtual machine and drag the .vmx file into your Virtual Machine -Library. -* Right-click on the virtual machine you just added to your library and -select "`Snapshots…`", and then click the "`Take`" button in the modal -that opens. You can roll back to this snapshot when the VM expires in 90 -days. -* In System Preferences > Sharing, change your computer name to be -something simple, e.g. "`computer`". -* Run {kib} with `yarn start --host=computer.local` (substituting -your computer name). -* Now you can run your VM, open the browser, and navigate to -`http://computer.local:5601` to test {kib}. -* Alternatively you can use browserstack [discrete] === Running browser automation tests @@ -93,4 +85,30 @@ include::development-functional-tests.asciidoc[leveloffset=+1] include::development-unit-tests.asciidoc[leveloffset=+1] -include::development-accessibility-tests.asciidoc[leveloffset=+1] \ No newline at end of file +include::development-accessibility-tests.asciidoc[leveloffset=+1] + +[discrete] +=== Cross-browser compatibility + +**Testing IE on OS X** + +**Note:** IE11 is not supported from 7.9 onwards. + +* http://www.vmware.com/products/fusion/fusion-evaluation.html[Download +VMWare Fusion]. +* https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/#downloads[Download +IE virtual machines] for VMWare. +* Open VMWare and go to Window > Virtual Machine Library. Unzip the +virtual machine and drag the .vmx file into your Virtual Machine +Library. +* Right-click on the virtual machine you just added to your library and +select "`Snapshots…`", and then click the "`Take`" button in the modal +that opens. You can roll back to this snapshot when the VM expires in 90 +days. +* In System Preferences > Sharing, change your computer name to be +something simple, e.g. "`computer`". +* Run {kib} with `yarn start --host=computer.local` (substituting +your computer name). +* Now you can run your VM, open the browser, and navigate to +`http://computer.local:5601` to test {kib}. +* Alternatively you can use browserstack \ No newline at end of file diff --git a/package.json b/package.json index 4fb88706be16f..ba6ac1e70248b 100644 --- a/package.json +++ b/package.json @@ -43,14 +43,12 @@ "preinstall": "node ./preinstall_check", "kbn": "node scripts/kbn", "es": "node scripts/es", - "test": "grunt test", "test:jest": "node scripts/jest", "test:jest_integration": "node scripts/jest_integration", "test:mocha": "node scripts/mocha", "test:ftr": "node scripts/functional_tests", "test:ftr:server": "node scripts/functional_tests_server", "test:ftr:runner": "node scripts/functional_test_runner", - "test:coverage": "grunt test:coverage", "checkLicenses": "node scripts/check_licenses --dev", "build": "node scripts/build --all-platforms", "start": "node scripts/kibana --dev", diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index 54b064f5cd49e..a88820eb281cc 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -62,3 +62,5 @@ export * from './functional_test_runner'; export { getUrl } from './jest/utils/get_url'; export { runCheckJestConfigsCli } from './jest/run_check_jest_configs_cli'; + +export { runJest } from './jest/run'; diff --git a/packages/kbn-test/src/jest/run.test.ts b/packages/kbn-test/src/jest/run.test.ts new file mode 100644 index 0000000000000..5be033baade6a --- /dev/null +++ b/packages/kbn-test/src/jest/run.test.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { commonBasePath } from './run'; + +describe('commonBasePath', () => { + it('returns a common path', () => { + expect(commonBasePath(['foo/bar/baz', 'foo/bar/quux', 'foo/bar'])).toBe('foo/bar'); + }); + + it('handles an empty array', () => { + expect(commonBasePath([])).toBe(''); + }); + + it('handles no common path', () => { + expect(commonBasePath(['foo', 'bar'])).toBe(''); + }); + + it('matches full paths', () => { + expect(commonBasePath(['foo/bar', 'foo/bar_baz'])).toBe('foo'); + }); +}); diff --git a/packages/kbn-test/src/jest/run.ts b/packages/kbn-test/src/jest/run.ts new file mode 100644 index 0000000000000..3283b6c8901fa --- /dev/null +++ b/packages/kbn-test/src/jest/run.ts @@ -0,0 +1,110 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Run Jest tests +// +// Provides Jest with `--config` to the first jest.config.js file found in the current +// directory, or while going up in the directory chain. If the current working directory +// is nested under the config path, a pattern will be provided to Jest to only run the +// tests within that directory. +// +// Any additional options passed will be forwarded to Jest. +// +// See all cli options in https://facebook.github.io/jest/docs/cli.html + +import { resolve, relative, sep as osSep } from 'path'; +import { existsSync } from 'fs'; +import { run } from 'jest'; +import { buildArgv } from 'jest-cli/build/cli'; +import { ToolingLog } from '@kbn/dev-utils'; + +// yarn test:jest src/core/server/saved_objects +// yarn test:jest src/core/public/core_system.test.ts +// :kibana/src/core/server/saved_objects yarn test:jest + +export function runJest(configName = 'jest.config.js') { + const argv = buildArgv(process.argv); + + const log = new ToolingLog({ + level: argv.verbose ? 'verbose' : 'info', + writeTo: process.stdout, + }); + + if (!argv.config) { + const cwd = process.env.INIT_CWD || process.cwd(); + const testFiles = argv._.splice(2).map((p) => resolve(cwd, p)); + const commonTestFiles = commonBasePath(testFiles); + const testFilesProvided = testFiles.length > 0; + + log.verbose('cwd:', cwd); + log.verbose('testFiles:', testFiles.join(', ')); + log.verbose('commonTestFiles:', commonTestFiles); + + let configPath; + + // sets the working directory to the cwd or the common + // base directory of the provided test files + let wd = testFilesProvided ? commonTestFiles : cwd; + + configPath = resolve(wd, configName); + + while (!existsSync(configPath)) { + wd = resolve(wd, '..'); + configPath = resolve(wd, configName); + } + + log.verbose(`no config provided, found ${configPath}`); + process.argv.push('--config', configPath); + + if (!testFilesProvided) { + log.verbose(`no test files provided, setting to current directory`); + process.argv.push(relative(wd, cwd)); + } + + log.info('yarn jest', process.argv.slice(2).join(' ')); + } + + if (process.env.NODE_ENV == null) { + process.env.NODE_ENV = 'test'; + } + + run(); +} + +/** + * Finds the common basePath by sorting the array + * and comparing the first and last element + */ +export function commonBasePath(paths: string[] = [], sep = osSep) { + if (paths.length === 0) return ''; + + paths = paths.concat().sort(); + + const first = paths[0].split(sep); + const last = paths[paths.length - 1].split(sep); + + const length = first.length; + let i = 0; + + while (i < length && first[i] === last[i]) { + i++; + } + + return first.slice(0, i).join(sep); +} diff --git a/scripts/jest.js b/scripts/jest.js index 90f8da10f4c90..cb31d7785898d 100755 --- a/scripts/jest.js +++ b/scripts/jest.js @@ -17,27 +17,4 @@ * under the License. */ -// # Run Jest tests -// -// All args will be forwarded directly to Jest, e.g. to watch tests run: -// -// node scripts/jest --watch -// -// or to build code coverage: -// -// node scripts/jest --coverage -// -// See all cli options in https://facebook.github.io/jest/docs/cli.html - -if (process.argv.indexOf('--config') === -1) { - // append correct jest.config if none is provided - var configPath = require('path').resolve(__dirname, '../jest.config.oss.js'); - process.argv.push('--config', configPath); - console.log('Running Jest with --config', configPath); -} - -if (process.env.NODE_ENV == null) { - process.env.NODE_ENV = 'test'; -} - -require('jest').run(); +require('@kbn/test').runJest(); diff --git a/scripts/jest_integration.js b/scripts/jest_integration.js index f07d28939ef0c..1df79781fe26d 100755 --- a/scripts/jest_integration.js +++ b/scripts/jest_integration.js @@ -17,29 +17,6 @@ * under the License. */ -// # Run Jest integration tests -// -// All args will be forwarded directly to Jest, e.g. to watch tests run: -// -// node scripts/jest_integration --watch -// -// or to build code coverage: -// -// node scripts/jest_integration --coverage -// -// See all cli options in https://facebook.github.io/jest/docs/cli.html - process.argv.push('--runInBand'); -if (process.argv.indexOf('--config') === -1) { - // append correct jest.config if none is provided - var configPath = require('path').resolve(__dirname, '../jest.config.integration.js'); - process.argv.push('--config', configPath); - console.log('Running Jest with --config', configPath); -} - -if (process.env.NODE_ENV == null) { - process.env.NODE_ENV = 'test'; -} - -require('jest').run(); +require('@kbn/test').runJest('jest.config.integration.js'); diff --git a/src/core/server/saved_objects/migrations/README.md b/src/core/server/saved_objects/migrations/README.md index 91249024358ac..7db0b38773a67 100644 --- a/src/core/server/saved_objects/migrations/README.md +++ b/src/core/server/saved_objects/migrations/README.md @@ -206,9 +206,13 @@ There are three core entry points. ## Testing -Run jest tests: +Run Jest tests: -`node scripts/jest --testPathPattern=migrations --watch` +Documentation: https://www.elastic.co/guide/en/kibana/current/development-tests.html#_unit_testing + +``` +yarn test:jest src/core/server/saved_objects/migrations --watch +``` Run integration tests: diff --git a/src/plugins/embeddable/README.asciidoc b/src/plugins/embeddable/README.asciidoc index 10ec2b840ffa7..5e3c5066f46c7 100644 --- a/src/plugins/embeddable/README.asciidoc +++ b/src/plugins/embeddable/README.asciidoc @@ -40,5 +40,5 @@ Run unit tests [source,sh] -- -node scripts/jest embeddable +yarn test:jest src/plugins/embeddable -- diff --git a/test/scripts/jenkins_unit.sh b/test/scripts/jenkins_unit.sh index c788a4a5b01ae..34b194ba5d4a7 100755 --- a/test/scripts/jenkins_unit.sh +++ b/test/scripts/jenkins_unit.sh @@ -35,7 +35,7 @@ if [[ -z "$CODE_COVERAGE" ]] ; then ./test/scripts/checks/test_hardening.sh else # echo " -> Running jest tests with coverage" - # node scripts/jest --ci --verbose --coverage + # node scripts/jest --ci --verbose --coverage --config jest.config.oss.js # rename_coverage_file "oss" # echo "" # echo "" diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index 438a85aa86142..6199aa0e5cdfc 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -4,23 +4,15 @@ source test/scripts/jenkins_test_setup.sh if [[ -z "$CODE_COVERAGE" ]] ; then echo " -> Running jest tests" - cd "$XPACK_DIR" - checks-reporter-with-killswitch "X-Pack Jest" node --max-old-space-size=6144 scripts/jest --ci --verbose - echo "" - echo "" - - # echo " -> Running jest integration tests" - # cd "$XPACK_DIR" - # node scripts/jest_integration --ci --verbose - # echo "" - # echo "" + + ./test/scripts/test/xpack_jest_unit.sh else echo " -> Running jest tests with coverage" - cd "$XPACK_DIR" + # build runtime for canvas echo "NODE_ENV=$NODE_ENV" - node ./plugins/canvas/scripts/shareable_runtime - node --max-old-space-size=6144 scripts/jest --ci --verbose --coverage + node ./x-pack/plugins/canvas/scripts/shareable_runtime + node --max-old-space-size=6144 scripts/jest x-pack --ci --verbose --coverage # rename file in order to be unique one test -f ../target/kibana-coverage/jest/coverage-final.json \ && mv ../target/kibana-coverage/jest/coverage-final.json \ diff --git a/test/scripts/test/jest_integration.sh b/test/scripts/test/jest_integration.sh index 8791248e9a166..78ed804f88430 100755 --- a/test/scripts/test/jest_integration.sh +++ b/test/scripts/test/jest_integration.sh @@ -3,4 +3,4 @@ source src/dev/ci_setup/setup_env.sh checks-reporter-with-killswitch "Jest Integration Tests" \ - node scripts/jest_integration + node scripts/jest_integration --ci --verbose diff --git a/test/scripts/test/jest_unit.sh b/test/scripts/test/jest_unit.sh index de5e16c2b1366..88c0fe528b88c 100755 --- a/test/scripts/test/jest_unit.sh +++ b/test/scripts/test/jest_unit.sh @@ -3,4 +3,4 @@ source src/dev/ci_setup/setup_env.sh checks-reporter-with-killswitch "Jest Unit Tests" \ - node scripts/jest + node scripts/jest --config jest.config.oss.js --ci --verbose --maxWorkers=5 diff --git a/test/scripts/test/xpack_jest_unit.sh b/test/scripts/test/xpack_jest_unit.sh index 93d70ec355391..33b1c8a2b5183 100755 --- a/test/scripts/test/xpack_jest_unit.sh +++ b/test/scripts/test/xpack_jest_unit.sh @@ -2,5 +2,5 @@ source src/dev/ci_setup/setup_env.sh -cd x-pack -checks-reporter-with-killswitch "X-Pack Jest" node --max-old-space-size=6144 scripts/jest --ci --verbose --maxWorkers=10 +checks-reporter-with-killswitch "X-Pack Jest" \ + node scripts/jest x-pack --ci --verbose --maxWorkers=5 diff --git a/x-pack/README.md b/x-pack/README.md index 0210b00d8efc8..41ea4cc4e469a 100644 --- a/x-pack/README.md +++ b/x-pack/README.md @@ -15,46 +15,11 @@ Example: `yarn es snapshot --license trial --password changeme` By default, this will also set the password for native realm accounts to the password provided (`changeme` by default). This includes that of the `kibana_system` user which `elasticsearch.username` defaults to in development. If you wish to specify a password for a given native realm account, you can do that like so: `--password.kibana_system=notsecure` # Testing -## Running specific tests -| Test runner | Test location | Runner command (working directory is kibana/x-pack) | -| ------------ | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | -| Jest | `x-pack/**/*.test.js`
`x-pack/**/*.test.ts` | `cd x-pack && node scripts/jest -t regexp [test path]` | -| Functional | `x-pack/test/*integration/**/config.js`
`x-pack/test/*functional/config.js`
`x-pack/test/accessibility/config.js` | `node scripts/functional_tests_server --config x-pack/test/[directory]/config.js`
`node scripts/functional_test_runner --config x-pack/test/[directory]/config.js --grep=regexp` | -Examples: - - Run the jest test case whose description matches 'filtering should skip values of null': - `cd x-pack && yarn test:jest -t 'filtering should skip values of null' plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js` - - Run the x-pack api integration test case whose description matches the given string: - `node scripts/functional_tests_server --config x-pack/test/api_integration/config.ts` - `node scripts/functional_test_runner --config x-pack/test/api_integration/config.ts --grep='apis Monitoring Beats list with restarted beat instance should load multiple clusters'` - -In addition to to providing a regular expression argument, specific tests can also be run by appeding `.only` to an `it` or `describe` function block. E.g. `describe(` to `describe.only(`. - -## Running all tests - -You can run unit tests by running: - -``` -yarn test -``` - -If you want to run tests only for a specific plugin (to save some time), you can run: - -``` -yarn test --plugins [,]* # where is "reporting", etc. -``` - -#### Running server unit tests -You can run mocha unit tests by running: - -``` -yarn test:mocha -``` +For information on testing, see [the Elastic functional test development guide](https://www.elastic.co/guide/en/kibana/current/development-functional-tests.html). #### Running functional tests -For more info, see [the Elastic functional test development guide](https://www.elastic.co/guide/en/kibana/current/development-tests.html#development-functional-tests). - The functional UI tests, the API integration tests, and the SAML API integration tests are all run against a live browser, Kibana, and Elasticsearch install. Each set of tests is specified with a unique config that describes how to start the Elasticsearch server, the Kibana server, and what tests to run against them. The sets of tests that exist today are *functional UI tests* ([specified by this config](test/functional/config.js)), *API integration tests* ([specified by this config](test/api_integration/config.ts)), and *SAML API integration tests* ([specified by this config](test/security_api_integration/saml.config.ts)). The script runs all sets of tests sequentially like so: @@ -116,7 +81,7 @@ node scripts/functional_tests --config test/security_api_integration/saml.config Jest integration tests can be used to test behavior with Elasticsearch and the Kibana server. ```sh -node scripts/jest_integration +yarn test:jest_integration ``` An example test exists at [test_utils/jest/integration_tests/example_integration.test.ts](test_utils/jest/integration_tests/example_integration.test.ts) diff --git a/x-pack/package.json b/x-pack/package.json index ddcb14348ed17..34ef8bb589b44 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -12,7 +12,7 @@ "build": "../node_modules/.bin/gulp build", "testonly": "echo 'Deprecated, use `yarn test`'", "test": "../node_modules/.bin/gulp test", - "test:jest": "node scripts/jest", + "test:jest": "node ../scripts/jest", "test:mocha": "node scripts/mocha" }, "kibana": { diff --git a/x-pack/plugins/beats_management/readme.md b/x-pack/plugins/beats_management/readme.md index 36db612f7affd..75adf428772e4 100644 --- a/x-pack/plugins/beats_management/readme.md +++ b/x-pack/plugins/beats_management/readme.md @@ -7,7 +7,13 @@ Failure to have auth enabled in Kibana will make for a broken UI. UI-based error ### Unit tests -From `~/kibana/x-pack`, run `node scripts/jest.js plugins/beats --watch`. +Run Jest tests: + +Documentation: https://www.elastic.co/guide/en/kibana/current/development-tests.html#_unit_testing + +``` +yarn test:jest x-pack/plugins/beats --watch +``` ### API tests diff --git a/x-pack/plugins/encrypted_saved_objects/README.md b/x-pack/plugins/encrypted_saved_objects/README.md index 0a5e79a96f02a..99ebf771126d5 100644 --- a/x-pack/plugins/encrypted_saved_objects/README.md +++ b/x-pack/plugins/encrypted_saved_objects/README.md @@ -235,9 +235,12 @@ const migration780 = encryptedSavedObjects.createMigration( ### Unit tests -From `kibana-root-folder/x-pack`, run: -```bash -$ node scripts/jest.js +Run Jest tests: + +Documentation: https://www.elastic.co/guide/en/kibana/current/development-tests.html#_unit_testing + +``` +yarn test:jest x-pack/plugins/encrypted_saved_objects --watch ``` ### API Integration tests diff --git a/x-pack/plugins/enterprise_search/README.md b/x-pack/plugins/enterprise_search/README.md index 711c7c7b065d2..3fd6154b1d265 100644 --- a/x-pack/plugins/enterprise_search/README.md +++ b/x-pack/plugins/enterprise_search/README.md @@ -29,10 +29,10 @@ To debug Kea state in-browser, Kea recommends [Redux Devtools](https://kea.js.or ### Unit tests -From `kibana-root-folder/x-pack`, run: +Documentation: https://www.elastic.co/guide/en/kibana/current/development-tests.html#_unit_testing -```bash -yarn test:jest plugins/enterprise_search +``` +yarn test:jest x-pack/plugins/enterprise_search --watch ``` ### E2E tests diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md index 941dedc3d1093..eb7fbc9d590fa 100644 --- a/x-pack/plugins/event_log/README.md +++ b/x-pack/plugins/event_log/README.md @@ -53,9 +53,10 @@ public setup(core: CoreSetup, { eventLog }: PluginSetupDependencies) { ### Unit tests -From `kibana-root-folder/x-pack`, run: -```bash -$ node node scripts/jest plugins/event_log +Documentation: https://www.elastic.co/guide/en/kibana/current/development-tests.html#_unit_testing + +``` +yarn test:jest x-pack/plugins/event_log --watch ``` ### API Integration tests diff --git a/x-pack/plugins/graph/README.md b/x-pack/plugins/graph/README.md index 9cc2617abe94c..99becabf70002 100644 --- a/x-pack/plugins/graph/README.md +++ b/x-pack/plugins/graph/README.md @@ -6,7 +6,7 @@ Graph shows only up in the side bar if your server is running on a platinum or t ## Common commands -* Run tests `node x-pack/scripts/jest.js --watch plugins/graph` +* Run tests `yarn test:jest x-pack/plugins/graph --watch` * Run type check `node scripts/type_check.js --project=x-pack/tsconfig.json` * Run linter `node scripts/eslint.js x-pack/plugins/graph` * Run functional tests (make sure to stop dev server) diff --git a/x-pack/plugins/lens/readme.md b/x-pack/plugins/lens/readme.md index 98bb60827af42..9fa6ad8ee30af 100644 --- a/x-pack/plugins/lens/readme.md +++ b/x-pack/plugins/lens/readme.md @@ -4,7 +4,7 @@ Run all tests from the `x-pack` root directory -- Unit tests: `node scripts/jest --watch lens` +- Unit tests: `yarn test:jest x-pack/plugins/lens` - Functional tests: - Run `node scripts/functional_tests_server` - Run `node ../scripts/functional_test_runner.js --config ./test/functional/config.js --grep="lens app"` diff --git a/x-pack/plugins/maps/README.md b/x-pack/plugins/maps/README.md index aae5a708b680b..729cba26f72ab 100644 --- a/x-pack/plugins/maps/README.md +++ b/x-pack/plugins/maps/README.md @@ -7,7 +7,7 @@ Visualize geo data from Elasticsearch or 3rd party geo-services. Run all tests from the `x-pack` root directory -- Unit tests: `node scripts/jest --watch maps` +- Unit tests: `yarn test:jest x-pack/plugins/maps --watch` - Functional tests: - Run `node scripts/functional_tests_server` - Run `node ../scripts/functional_test_runner.js --config ./test/functional/config.js --grep="maps app"` \ No newline at end of file diff --git a/x-pack/plugins/ml/readme.md b/x-pack/plugins/ml/readme.md index 2369f3d077037..b97e7cbc2ee04 100644 --- a/x-pack/plugins/ml/readme.md +++ b/x-pack/plugins/ml/readme.md @@ -68,30 +68,32 @@ These data sets are now ready be analyzed in ML jobs in Kibana. ### Jest tests -Run the test following jest tests from `kibana/x-pack`. +Documentation: https://www.elastic.co/guide/en/kibana/current/development-tests.html#_unit_testing + +Run the test following jest tests from `kibana/x-pack/plugins/ml`. New snapshots, all plugins: ``` -node scripts/jest +yarn test:jest ``` Update snapshots for the ML plugin: ``` -node scripts/jest plugins/ml -u +yarn test:jest -u ``` Update snapshots for a specific directory only: ``` -node scripts/jest plugins/ml/public/application/settings/filter_lists +yarn test:jest public/application/settings/filter_lists ``` Run tests with verbose output: ``` -node scripts/jest plugins/ml --verbose +yarn test:jest --verbose ``` ### Functional tests diff --git a/x-pack/plugins/task_manager/README.md b/x-pack/plugins/task_manager/README.md index d3c8ecb6c4505..6cd42cda9af6a 100644 --- a/x-pack/plugins/task_manager/README.md +++ b/x-pack/plugins/task_manager/README.md @@ -505,9 +505,11 @@ The task manager's public API is create / delete / list. Updates aren't directly ## Testing - Unit tests: + + Documentation: https://www.elastic.co/guide/en/kibana/current/development-tests.html#_unit_testing + ``` - cd x-pack - node scripts/jest --testPathPattern=task_manager --watch + yarn test:jest x-pack/plugins/task_manager --watch ``` - Integration tests: ``` diff --git a/x-pack/plugins/transform/readme.md b/x-pack/plugins/transform/readme.md index 07500876f55c2..a1005c43687e2 100644 --- a/x-pack/plugins/transform/readme.md +++ b/x-pack/plugins/transform/readme.md @@ -67,30 +67,32 @@ These data sets are now ready to be used for creating transforms in Kibana. ### Jest tests -Run the test following jest tests from `kibana/x-pack`. +Documentation: https://www.elastic.co/guide/en/kibana/current/development-tests.html#_unit_testing + +Run the test following jest tests from `kibana/x-pack/plugins/transform. New snapshots, all plugins: ``` -node scripts/jest +yarn test:jest ``` Update snapshots for the transform plugin: ``` -node scripts/jest plugins/transform -u +yarn test:jest -u ``` Update snapshots for a specific directory only: ``` -node scripts/jest x-pack/plugins/transform/public/app/sections +yarn test:jest public/app/sections ``` Run tests with verbose output: ``` -node scripts/jest plugins/transform --verbose +yarn test:jest --verbose ``` ### Functional tests diff --git a/x-pack/plugins/uptime/README.md b/x-pack/plugins/uptime/README.md index 54bf48e8d3c86..34d206a1fdcb8 100644 --- a/x-pack/plugins/uptime/README.md +++ b/x-pack/plugins/uptime/README.md @@ -42,7 +42,11 @@ There's also a `rest_api` folder that defines the structure of the RESTful API e ### Unit tests -From `~/kibana/x-pack`, run `node scripts/jest.js`. +Documentation: https://www.elastic.co/guide/en/kibana/current/development-tests.html#_unit_testing + +``` +yarn test:jest x-pack/plugins/uptime +``` ### Functional tests diff --git a/x-pack/scripts/jest.js b/x-pack/scripts/jest.js index 68cfcf082f818..aca7e558301df 100644 --- a/x-pack/scripts/jest.js +++ b/x-pack/scripts/jest.js @@ -4,15 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -if (process.argv.indexOf('--config') === -1) { - // append correct jest.config if none is provided - const configPath = require('path').resolve(__dirname, '../jest.config.js'); - process.argv.push('--config', configPath); - console.log('Running Jest with --config', configPath); -} - -if (process.env.NODE_ENV == null) { - process.env.NODE_ENV = 'test'; -} - -require('jest').run(); +require('@kbn/test').runJest(); From f6cd2648afa02050de32991c5a463dc44cf22926 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Mon, 14 Dec 2020 17:09:34 -0500 Subject: [PATCH 25/44] [Security_Solution][Endpoint] Register Custom tab into Fleet Endpoint Integration Detail (#85643) * Fleet: add component props to the Package Custom UI extension * Endpoint: Register UI Extension with fleet for endpoint custom content * Endpoint: UI for Trusted Apps custom entry --- .../sections/epm/screens/detail/content.tsx | 33 +++-- .../applications/fleet/types/ui_extensions.ts | 10 +- x-pack/plugins/fleet/public/index.ts | 1 + .../components/endpoint/link_to_app.tsx | 2 +- .../components/fleet_trusted_apps_card.tsx | 92 ++++++++++++ .../components/link_with_icon.tsx | 29 ++++ .../components/trusted_app_items_summary.tsx | 68 +++++++++ .../index.tsx | 21 +++ .../endpoint_policy_edit_extension.tsx | 136 +----------------- ...lazy_endpoint_package_custom_extension.tsx | 29 ++++ .../security_solution/public/plugin.tsx | 7 + .../apps/endpoint/fleet_integrations.ts | 40 ++++++ .../apps/endpoint/index.ts | 1 + .../apps/endpoint/policy_details.ts | 29 ---- .../page_objects/fleet_integrations_page.ts | 32 +++++ .../page_objects/index.ts | 2 + .../services/endpoint_policy.ts | 11 ++ 17 files changed, 367 insertions(+), 176 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/link_with_icon.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/trusted_app_items_summary.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension.tsx create mode 100644 x-pack/test/security_solution_endpoint/apps/endpoint/fleet_integrations.ts create mode 100644 x-pack/test/security_solution_endpoint/page_objects/fleet_integrations_page.ts diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx index b19a82d3100c5..f0051fb54e124 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx @@ -5,11 +5,11 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import React from 'react'; +import React, { memo, useMemo } from 'react'; import styled from 'styled-components'; import { Redirect } from 'react-router-dom'; import { DetailParams } from '.'; -import { PackageInfo } from '../../../../types'; +import { DetailViewPanelName, PackageInfo } from '../../../../types'; import { AssetsFacetGroup } from '../../components/assets_facet_group'; import { CenterColumn, LeftColumn, RightColumn } from './layout'; import { OverviewPanel } from './overview_panel'; @@ -18,6 +18,7 @@ import { SettingsPanel } from './settings_panel'; import { useUIExtension } from '../../../../hooks/use_ui_extension'; import { ExtensionWrapper } from '../../../../components/extension_wrapper'; import { useLink } from '../../../../hooks'; +import { pkgKeyFromPackageInfo } from '../../../../services/pkg_key_from_package_info'; type ContentProps = PackageInfo & Pick; @@ -34,12 +35,17 @@ const ContentFlexGroup = styled(EuiFlexGroup)` `; export function Content(props: ContentProps) { - const showRightColumn = props.panel !== 'policies'; + const { panel } = props; + const showRightColumn = useMemo(() => { + const fullWidthContentPages: DetailViewPanelName[] = ['policies', 'custom']; + return !fullWidthContentPages.includes(panel!); + }, [panel]); + return ( - + {showRightColumn && ( @@ -50,9 +56,14 @@ export function Content(props: ContentProps) { ); } -type ContentPanelProps = PackageInfo & Pick; -export function ContentPanel(props: ContentPanelProps) { - const { panel, name, version, assets, title, removable, latestVersion } = props; +interface ContentPanelProps { + packageInfo: PackageInfo; + panel: DetailViewPanelName; +} +export const ContentPanel = memo(({ panel, packageInfo }) => { + const { name, version, assets, title, removable, latestVersion } = packageInfo; + const pkgkey = pkgKeyFromPackageInfo(packageInfo); + const CustomView = useUIExtension(name, 'package-detail-custom'); const { getPath } = useLink(); @@ -73,16 +84,16 @@ export function ContentPanel(props: ContentPanelProps) { case 'custom': return CustomView ? ( - + ) : ( - + ); case 'overview': default: - return ; + return ; } -} +}); type RightColumnContentProps = PackageInfo & Pick; function RightColumnContent(props: RightColumnContentProps) { diff --git a/x-pack/plugins/fleet/public/applications/fleet/types/ui_extensions.ts b/x-pack/plugins/fleet/public/applications/fleet/types/ui_extensions.ts index d35e5f4744449..b3a741806aa88 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/types/ui_extensions.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/types/ui_extensions.ts @@ -5,7 +5,7 @@ */ import { ComponentType, LazyExoticComponent } from 'react'; -import { NewPackagePolicy, PackagePolicy } from './index'; +import { NewPackagePolicy, PackageInfo, PackagePolicy } from './index'; /** Register a Fleet UI extension */ export type UIExtensionRegistrationCallback = (extensionPoint: UIExtensionPoint) => void; @@ -80,7 +80,13 @@ export interface PackagePolicyCreateExtension { /** * UI Component Extension is used to display a Custom tab (and view) under a given Integration */ -export type PackageCustomExtensionComponent = ComponentType; +export type PackageCustomExtensionComponent = ComponentType; + +export interface PackageCustomExtensionComponentProps { + /** The package key value that should be used used for URLs */ + pkgkey: string; + packageInfo: PackageInfo; +} /** Extension point registration contract for Integration details Custom view */ export interface PackageCustomExtension { diff --git a/x-pack/plugins/fleet/public/index.ts b/x-pack/plugins/fleet/public/index.ts index be53af77f4b46..6271a0713cbdb 100644 --- a/x-pack/plugins/fleet/public/index.ts +++ b/x-pack/plugins/fleet/public/index.ts @@ -17,3 +17,4 @@ export * from './applications/fleet/types/intra_app_route_state'; export * from './applications/fleet/types/ui_extensions'; export { pagePathGetters } from './applications/fleet/constants'; +export { pkgKeyFromPackageInfo } from './applications/fleet/services/pkg_key_from_package_info'; diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/link_to_app.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/link_to_app.tsx index 66cfb0398b44c..f4a3306f45e36 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/link_to_app.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/link_to_app.tsx @@ -8,7 +8,7 @@ import React, { memo, MouseEventHandler } from 'react'; import { EuiLink, EuiLinkProps, EuiButton, EuiButtonProps } from '@elastic/eui'; import { useNavigateToAppEventHandler } from '../../hooks/endpoint/use_navigate_to_app_event_handler'; -type LinkToAppProps = (EuiLinkProps | EuiButtonProps) & { +export type LinkToAppProps = (EuiLinkProps | EuiButtonProps) & { /** the app id - normally the value of the `id` in that plugin's `kibana.json` */ appId: string; /** Any app specific path (route) */ diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx new file mode 100644 index 0000000000000..3dc0356bfd518 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useMemo } from 'react'; +import { ApplicationStart } from 'kibana/public'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + PackageCustomExtensionComponentProps, + pagePathGetters, +} from '../../../../../../../../../fleet/public'; +import { useKibana } from '../../../../../../../../../../../src/plugins/kibana_react/public'; +import { getTrustedAppsListPath } from '../../../../../../common/routing'; +import { TrustedAppsListPageRouteState } from '../../../../../../../../common/endpoint/types'; +import { PLUGIN_ID as FLEET_PLUGIN_ID } from '../../../../../../../../../fleet/common'; +import { MANAGEMENT_APP_ID } from '../../../../../../common/constants'; +import { LinkWithIcon } from './link_with_icon'; +import { TrustedAppItemsSummary } from './trusted_app_items_summary'; + +export const FleetTrustedAppsCard = memo(({ pkgkey }) => { + const { + services: { + application: { getUrlForApp }, + }, + } = useKibana<{ application: ApplicationStart }>(); + + const trustedAppsListUrlPath = getTrustedAppsListPath(); + + const trustedAppRouteState = useMemo(() => { + const fleetPackageCustomUrlPath = `#${pagePathGetters.integration_details({ + pkgkey, + panel: 'custom', + })}`; + return { + backButtonLabel: i18n.translate( + 'xpack.securitySolution.endpoint.fleetCustomExtension.backButtonLabel', + { defaultMessage: 'Back to Endpoint Integration' } + ), + onBackButtonNavigateTo: [ + FLEET_PLUGIN_ID, + { + path: fleetPackageCustomUrlPath, + }, + ], + backButtonUrl: getUrlForApp(FLEET_PLUGIN_ID, { + path: fleetPackageCustomUrlPath, + }), + }; + }, [getUrlForApp, pkgkey]); + + return ( + + + + +

+ +

+
+
+ + + + + + + + + + +
+
+ ); +}); + +FleetTrustedAppsCard.displayName = 'FleetTrustedAppsCard'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/link_with_icon.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/link_with_icon.tsx new file mode 100644 index 0000000000000..954c30670bbe4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/link_with_icon.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import styled from 'styled-components'; +import React, { FC, memo } from 'react'; +import { EuiIcon } from '@elastic/eui'; +import { + LinkToApp, + LinkToAppProps, +} from '../../../../../../../common/components/endpoint/link_to_app'; + +const LinkLabel = styled.span` + display: inline-block; + padding-right: ${(props) => props.theme.eui.paddingSizes.s}; +`; + +export const LinkWithIcon: FC = memo(({ children, ...props }) => { + return ( + + {children} + + + ); +}); + +LinkWithIcon.displayName = 'LinkWithIcon'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/trusted_app_items_summary.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/trusted_app_items_summary.tsx new file mode 100644 index 0000000000000..f0d3b51d20d67 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/trusted_app_items_summary.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import React, { FC, memo, useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { CoreStart } from 'kibana/public'; +import { useKibana } from '../../../../../../../../../../../src/plugins/kibana_react/public'; +import { TrustedAppsHttpService } from '../../../../../trusted_apps/service'; + +export const TrustedAppItemsSummary = memo(() => { + const { + services: { http }, + } = useKibana(); + const [total, setTotal] = useState(0); + const [trustedAppsApi] = useState(() => new TrustedAppsHttpService(http)); + + useEffect(() => { + trustedAppsApi + .getTrustedAppsList({ + page: 1, + per_page: 1, + }) + .then((response) => { + setTotal(response.total); + }); + }, [trustedAppsApi]); + + return ( +
+ + + +
+ ); +}); + +TrustedAppItemsSummary.displayName = 'TrustedAppItemsSummary'; + +const SummaryStat: FC<{ value: number; color?: EuiBadgeProps['color'] }> = memo( + ({ children, value, color, ...commonProps }) => { + return ( + + + + {children} + + + {value} + + + + ); + } +); + +SummaryStat.displayName = 'SummaryState'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx new file mode 100644 index 0000000000000..385ddeab33feb --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import { PackageCustomExtensionComponentProps } from '../../../../../../../../fleet/public'; +import { FleetTrustedAppsCard } from './components/fleet_trusted_apps_card'; + +export const EndpointPackageCustomExtension = memo( + (props) => { + return ( +
+ +
+ ); + } +); + +EndpointPackageCustomExtension.displayName = 'EndpointPackageCustomExtension'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx index 6d464280b2763..916a11ac65dd1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx @@ -4,35 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiCallOut, - EuiText, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiContextMenuPanel, - EuiPopover, - EuiButton, - EuiContextMenuItem, - EuiContextMenuPanelProps, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import React, { memo, useEffect, useState } from 'react'; +import { EuiSpacer } from '@elastic/eui'; import { useDispatch } from 'react-redux'; import { - pagePathGetters, PackagePolicyEditExtensionComponentProps, NewPackagePolicy, } from '../../../../../../../fleet/public'; -import { getPolicyDetailPath, getTrustedAppsListPath } from '../../../../common/routing'; -import { MANAGEMENT_APP_ID } from '../../../../common/constants'; -import { - PolicyDetailsRouteState, - TrustedAppsListPageRouteState, -} from '../../../../../../common/endpoint/types'; -import { useKibana } from '../../../../../common/lib/kibana'; -import { useNavigateToAppEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; +import { getPolicyDetailPath } from '../../../../common/routing'; import { PolicyDetailsForm } from '../policy_details_form'; import { AppAction } from '../../../../../common/store/actions'; import { usePolicyDetailsSelector } from '../policy_hooks'; @@ -46,12 +25,6 @@ export const EndpointPolicyEditExtension = memo { return ( <> - - - - - - @@ -126,106 +99,3 @@ const WrappedPolicyDetailsForm = memo<{ ); }); WrappedPolicyDetailsForm.displayName = 'WrappedPolicyDetailsForm'; - -const EditFlowMessage = memo<{ - agentPolicyId: string; - integrationPolicyId: string; -}>(({ agentPolicyId, integrationPolicyId }) => { - const { - services: { - application: { getUrlForApp }, - }, - } = useKibana(); - - const [isMenuOpen, setIsMenuOpen] = useState(false); - - const navigateBackToIngest = useMemo< - PolicyDetailsRouteState['onSaveNavigateTo'] & - PolicyDetailsRouteState['onCancelNavigateTo'] & - TrustedAppsListPageRouteState['onBackButtonNavigateTo'] - >(() => { - return [ - 'fleet', - { - path: `#${pagePathGetters.edit_integration({ - policyId: agentPolicyId, - packagePolicyId: integrationPolicyId!, - })}`, - }, - ]; - }, [agentPolicyId, integrationPolicyId]); - - const handleClosePopup = useCallback(() => setIsMenuOpen(false), []); - - const handleTrustedAppsAction = useNavigateToAppEventHandler( - MANAGEMENT_APP_ID, - { - path: getTrustedAppsListPath(), - state: { - backButtonUrl: navigateBackToIngest[1]?.path - ? `${getUrlForApp('fleet')}${navigateBackToIngest[1].path}` - : undefined, - onBackButtonNavigateTo: navigateBackToIngest, - backButtonLabel: i18n.translate( - 'xpack.securitySolution.endpoint.fleet.editPackagePolicy.trustedAppsMessageReturnBackLabel', - { defaultMessage: 'Back to Edit Integration' } - ), - }, - } - ); - - const menuButton = useMemo(() => { - return ( - setIsMenuOpen((prevState) => !prevState)} - data-test-subj="endpointActions" - > - - - ); - }, []); - - const actionItems = useMemo(() => { - return [ - - - , - ]; - }, [handleTrustedAppsAction]); - - return ( - - - - - - - - - - - ); -}); -EditFlowMessage.displayName = 'EditFlowMessage'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension.tsx new file mode 100644 index 0000000000000..d9616b3862893 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreStart } from 'kibana/public'; +import { lazy } from 'react'; +import { StartPlugins } from '../../../../../types'; +import { PackageCustomExtensionComponent } from '../../../../../../../fleet/public'; + +export const getLazyEndpointPackageCustomExtension = ( + coreStart: CoreStart, + depsStart: Pick +) => { + return lazy(async () => { + const [{ withSecurityContext }, { EndpointPackageCustomExtension }] = await Promise.all([ + import('./with_security_context'), + import('./endpoint_package_custom_extension'), + ]); + return { + default: withSecurityContext({ + coreStart, + depsStart, + WrappedComponent: EndpointPackageCustomExtension, + }), + }; + }); +}; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 0b5093ff50c39..015a965be8f78 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -64,6 +64,7 @@ import { getCaseConnectorUI } from './cases/components/connectors'; import { licenseService } from './common/hooks/use_license'; import { getLazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension'; import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension'; +import { getLazyEndpointPackageCustomExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension'; export class Plugin implements IPlugin { private kibanaVersion: string; @@ -346,6 +347,12 @@ export class Plugin implements IPlugin { + beforeEach(async () => { + await fleetIntegrations.navigateToIntegrationDetails( + await policyTestResources.getEndpointPkgKey() + ); + }); + + it('should show the Custom tab', async () => { + await fleetIntegrations.integrationDetailCustomTabExistsOrFail(); + }); + + it('should display the endpoint custom content', async () => { + await (await fleetIntegrations.findIntegrationDetailCustomTab()).click(); + await testSubjects.existOrFail('fleetEndpointPackageCustomContent'); + }); + + it('should show the Trusted Apps page when link is clicked', async () => { + await (await fleetIntegrations.findIntegrationDetailCustomTab()).click(); + await (await testSubjects.find('linkToTrustedApps')).click(); + await trustedApps.ensureIsOnTrustedAppsListPage(); + }); + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts index 3103d461669f1..bb740ef8acb88 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts @@ -33,5 +33,6 @@ export default function (providerContext: FtrProviderContext) { loadTestFile(require.resolve('./resolver')); loadTestFile(require.resolve('./endpoint_telemetry')); loadTestFile(require.resolve('./trusted_apps_list')); + loadTestFile(require.resolve('./fleet_integrations')); }); } diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index 355e494cb459e..1a5c99294c281 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -553,35 +553,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { } }); - it('should show callout', async () => { - await testSubjects.existOrFail('endpointPackagePolicy_edit'); - }); - - it('should show actions button with expected action items', async () => { - const actionsButton = await pageObjects.ingestManagerCreatePackagePolicy.findEndpointActionsButton(); - await actionsButton.click(); - const menuPanel = await testSubjects.find('endpointActionsMenuPanel'); - const actionItems = await menuPanel.findAllByTagName<'button'>('button'); - const expectedItems = ['Edit Trusted Applications']; - - for (const action of actionItems) { - const buttonText = await action.getVisibleText(); - expect(buttonText).to.be(expectedItems.find((item) => item === buttonText)); - } - }); - - it('should navigate to Trusted Apps', async () => { - await pageObjects.ingestManagerCreatePackagePolicy.selectEndpointAction('trustedApps'); - await pageObjects.trustedApps.ensureIsOnTrustedAppsListPage(); - }); - - it('should show the back button on Trusted Apps Page and navigate back to fleet', async () => { - await pageObjects.ingestManagerCreatePackagePolicy.selectEndpointAction('trustedApps'); - const backButton = await pageObjects.trustedApps.findTrustedAppsListPageBackButton(); - await backButton.click(); - await pageObjects.ingestManagerCreatePackagePolicy.ensureOnEditPageOrFail(); - }); - it('should show the endpoint policy form', async () => { await testSubjects.existOrFail('endpointIntegrationPolicyForm'); }); diff --git a/x-pack/test/security_solution_endpoint/page_objects/fleet_integrations_page.ts b/x-pack/test/security_solution_endpoint/page_objects/fleet_integrations_page.ts new file mode 100644 index 0000000000000..3c747afab48c8 --- /dev/null +++ b/x-pack/test/security_solution_endpoint/page_objects/fleet_integrations_page.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; +import { PLUGIN_ID } from '../../../plugins/fleet/common'; + +// NOTE: import path below should be the deep path to the actual module - else we get CI errors +import { pagePathGetters } from '../../../plugins/fleet/public/applications/fleet/constants/page_paths'; + +export function FleetIntegrations({ getService, getPageObjects }: FtrProviderContext) { + const pageObjects = getPageObjects(['common']); + const testSubjects = getService('testSubjects'); + + return { + async navigateToIntegrationDetails(pkgkey: string) { + await pageObjects.common.navigateToApp(PLUGIN_ID, { + hash: pagePathGetters.integration_details({ pkgkey }), + }); + }, + + async integrationDetailCustomTabExistsOrFail() { + await testSubjects.existOrFail('tab-custom'); + }, + + async findIntegrationDetailCustomTab() { + return await testSubjects.find('tab-custom'); + }, + }; +} diff --git a/x-pack/test/security_solution_endpoint/page_objects/index.ts b/x-pack/test/security_solution_endpoint/page_objects/index.ts index 3664a2033d8b7..2fb441464e7ee 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/index.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/index.ts @@ -11,6 +11,7 @@ import { TrustedAppsPageProvider } from './trusted_apps_page'; import { EndpointPageUtils } from './page_utils'; import { IngestManagerCreatePackagePolicy } from './ingest_manager_create_package_policy_page'; import { SecurityHostsPageProvider } from './hosts_page'; +import { FleetIntegrations } from './fleet_integrations_page'; export const pageObjects = { ...xpackFunctionalPageObjects, @@ -20,4 +21,5 @@ export const pageObjects = { endpointPageUtils: EndpointPageUtils, ingestManagerCreatePackagePolicy: IngestManagerCreatePackagePolicy, hosts: SecurityHostsPageProvider, + fleetIntegrations: FleetIntegrations, }; diff --git a/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts b/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts index 1b1d0bf96a187..5f54ab2539c5d 100644 --- a/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts +++ b/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts @@ -19,6 +19,9 @@ import { import { factory as policyConfigFactory } from '../../../plugins/security_solution/common/endpoint/models/policy_config'; import { Immutable } from '../../../plugins/security_solution/common/endpoint/types'; +// NOTE: import path below should be the deep path to the actual module - else we get CI errors +import { pkgKeyFromPackageInfo } from '../../../plugins/fleet/public/applications/fleet/services/pkg_key_from_package_info'; + const INGEST_API_ROOT = '/api/fleet'; const INGEST_API_AGENT_POLICIES = `${INGEST_API_ROOT}/agent_policies`; const INGEST_API_AGENT_POLICIES_DELETE = `${INGEST_API_AGENT_POLICIES}/delete`; @@ -106,6 +109,14 @@ export function EndpointPolicyTestResourcesProvider({ getService }: FtrProviderC })(); return { + /** + * Returns the endpoint package key for the currently installed package. This `pkgkey` can then + * be used to build URLs for Fleet pages or APIs + */ + async getEndpointPkgKey() { + return pkgKeyFromPackageInfo((await retrieveEndpointPackageInfo())!); + }, + /** * Retrieves the full Agent policy, which mirrors what the Elastic Agent would get * once they checkin. From eee237baff9b07314ac0250b2ac7bd519ae40c46 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Mon, 14 Dec 2020 16:10:56 -0600 Subject: [PATCH 26/44] skip 'alerts should delete all selection' #77401 --- .../apps/triggers_actions_ui/alerts_list.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index f7281a1d93a46..16b338c893736 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -349,7 +349,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await deleteAlerts([createdAlert.id]); }); - it('should delete all selection', async () => { + it.skip('should delete all selection', async () => { const namePrefix = generateUniqueKey(); let count = 0; const createdAlertsFirstPage = await Promise.all( From dd34712ce6400aac4fb307a7b6152050e75230b8 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 14 Dec 2020 17:12:05 -0500 Subject: [PATCH 27/44] Update core usage stats collection (#85706) --- .../core_usage_data_service.test.ts | 22 +- .../core_usage_data_service.ts | 7 +- .../core_usage_stats_client.mock.ts | 8 + .../core_usage_stats_client.test.ts | 757 +++++++++++++++++- .../core_usage_stats_client.ts | 176 ++-- src/core/server/core_usage_data/types.ts | 82 +- .../saved_objects/routes/bulk_create.ts | 11 +- .../server/saved_objects/routes/bulk_get.ts | 10 +- .../saved_objects/routes/bulk_update.ts | 10 +- .../server/saved_objects/routes/create.ts | 10 +- .../server/saved_objects/routes/delete.ts | 11 +- .../server/saved_objects/routes/export.ts | 3 +- src/core/server/saved_objects/routes/find.ts | 10 +- src/core/server/saved_objects/routes/get.ts | 11 +- .../server/saved_objects/routes/import.ts | 3 +- src/core/server/saved_objects/routes/index.ts | 16 +- .../integration_tests/bulk_create.test.ts | 14 +- .../routes/integration_tests/bulk_get.test.ts | 14 +- .../integration_tests/bulk_update.test.ts | 14 +- .../routes/integration_tests/create.test.ts | 14 +- .../routes/integration_tests/delete.test.ts | 14 +- .../routes/integration_tests/export.test.ts | 4 +- .../routes/integration_tests/find.test.ts | 14 +- .../routes/integration_tests/get.test.ts | 14 +- .../routes/integration_tests/import.test.ts | 4 +- .../resolve_import_errors.test.ts | 4 +- .../routes/integration_tests/update.test.ts | 14 +- .../routes/resolve_import_errors.ts | 3 +- .../server/saved_objects/routes/update.ts | 10 +- src/core/server/server.api.md | 148 +++- src/core/server/server.ts | 1 + .../collectors/core/core_usage_collector.ts | 90 ++- src/plugins/telemetry/schema/oss_plugins.json | 216 ++++- .../apis/saved_objects/bulk_create.js | 6 +- .../apis/saved_objects/bulk_update.js | 4 +- .../apis/saved_objects/create.js | 4 +- .../apis/saved_objects/update.js | 2 +- .../usage_stats/usage_stats_client.test.ts | 2 +- .../server/usage_stats/usage_stats_client.ts | 4 +- 39 files changed, 1590 insertions(+), 171 deletions(-) diff --git a/src/core/server/core_usage_data/core_usage_data_service.test.ts b/src/core/server/core_usage_data/core_usage_data_service.test.ts index e22dfcb1e3a20..737c851f03bc9 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.test.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.test.ts @@ -29,6 +29,7 @@ import { config as RawHttpConfig } from '../http/http_config'; import { config as RawLoggingConfig } from '../logging/logging_config'; import { config as RawKibanaConfig } from '../kibana_config'; import { savedObjectsConfig as RawSavedObjectsConfig } from '../saved_objects/saved_objects_config'; +import { httpServiceMock } from '../http/http_service.mock'; import { metricsServiceMock } from '../metrics/metrics_service.mock'; import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; @@ -68,11 +69,12 @@ describe('CoreUsageDataService', () => { describe('setup', () => { it('creates internal repository', async () => { + const http = httpServiceMock.createInternalSetupContract(); const metrics = metricsServiceMock.createInternalSetupContract(); const savedObjectsStartPromise = Promise.resolve( savedObjectsServiceMock.createStartContract() ); - service.setup({ metrics, savedObjectsStartPromise }); + service.setup({ http, metrics, savedObjectsStartPromise }); const savedObjects = await savedObjectsStartPromise; expect(savedObjects.createInternalRepository).toHaveBeenCalledTimes(1); @@ -81,14 +83,12 @@ describe('CoreUsageDataService', () => { describe('#registerType', () => { it('registers core usage stats type', async () => { + const http = httpServiceMock.createInternalSetupContract(); const metrics = metricsServiceMock.createInternalSetupContract(); const savedObjectsStartPromise = Promise.resolve( savedObjectsServiceMock.createStartContract() ); - const coreUsageData = service.setup({ - metrics, - savedObjectsStartPromise, - }); + const coreUsageData = service.setup({ http, metrics, savedObjectsStartPromise }); const typeRegistry = typeRegistryMock.create(); coreUsageData.registerType(typeRegistry); @@ -104,14 +104,12 @@ describe('CoreUsageDataService', () => { describe('#getClient', () => { it('returns client', async () => { + const http = httpServiceMock.createInternalSetupContract(); const metrics = metricsServiceMock.createInternalSetupContract(); const savedObjectsStartPromise = Promise.resolve( savedObjectsServiceMock.createStartContract() ); - const coreUsageData = service.setup({ - metrics, - savedObjectsStartPromise, - }); + const coreUsageData = service.setup({ http, metrics, savedObjectsStartPromise }); const usageStatsClient = coreUsageData.getClient(); expect(usageStatsClient).toBeInstanceOf(CoreUsageStatsClient); @@ -122,11 +120,12 @@ describe('CoreUsageDataService', () => { describe('start', () => { describe('getCoreUsageData', () => { it('returns core metrics for default config', async () => { + const http = httpServiceMock.createInternalSetupContract(); const metrics = metricsServiceMock.createInternalSetupContract(); const savedObjectsStartPromise = Promise.resolve( savedObjectsServiceMock.createStartContract() ); - service.setup({ metrics, savedObjectsStartPromise }); + service.setup({ http, metrics, savedObjectsStartPromise }); const elasticsearch = elasticsearchServiceMock.createStart(); elasticsearch.client.asInternalUser.cat.indices.mockResolvedValueOnce({ body: [ @@ -296,6 +295,7 @@ describe('CoreUsageDataService', () => { observables.push(newObservable); return newObservable; }); + const http = httpServiceMock.createInternalSetupContract(); const metrics = metricsServiceMock.createInternalSetupContract(); metrics.getOpsMetrics$.mockImplementation(() => { const newObservable = hot('-a-------'); @@ -306,7 +306,7 @@ describe('CoreUsageDataService', () => { savedObjectsServiceMock.createStartContract() ); - service.setup({ metrics, savedObjectsStartPromise }); + service.setup({ http, metrics, savedObjectsStartPromise }); // Use the stopTimer$ to delay calling stop() until the third frame const stopTimer$ = cold('---a|'); diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts index 02b4f2ac59133..07c583186b453 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -24,7 +24,7 @@ import { CoreService } from 'src/core/types'; import { Logger, SavedObjectsServiceStart, SavedObjectTypeRegistry } from 'src/core/server'; import { CoreContext } from '../core_context'; import { ElasticsearchConfigType } from '../elasticsearch/elasticsearch_config'; -import { HttpConfigType } from '../http'; +import { HttpConfigType, InternalHttpServiceSetup } from '../http'; import { LoggingConfigType } from '../logging'; import { SavedObjectsConfigType } from '../saved_objects/saved_objects_config'; import { @@ -42,6 +42,7 @@ import { CoreUsageStatsClient } from './core_usage_stats_client'; import { MetricsServiceSetup, OpsMetrics } from '..'; export interface SetupDeps { + http: InternalHttpServiceSetup; metrics: MetricsServiceSetup; savedObjectsStartPromise: Promise; } @@ -248,7 +249,7 @@ export class CoreUsageDataService implements CoreService { const debugLogger = (message: string) => this.logger.debug(message); - return new CoreUsageStatsClient(debugLogger, internalRepositoryPromise); + return new CoreUsageStatsClient(debugLogger, http.basePath, internalRepositoryPromise); }; this.coreUsageStatsClient = getClient(); diff --git a/src/core/server/core_usage_data/core_usage_stats_client.mock.ts b/src/core/server/core_usage_data/core_usage_stats_client.mock.ts index 3bfb411c9dd49..ef350a9bb4c5c 100644 --- a/src/core/server/core_usage_data/core_usage_stats_client.mock.ts +++ b/src/core/server/core_usage_data/core_usage_stats_client.mock.ts @@ -22,6 +22,14 @@ import { CoreUsageStatsClient } from '.'; const createUsageStatsClientMock = () => (({ getUsageStats: jest.fn().mockResolvedValue({}), + incrementSavedObjectsBulkCreate: jest.fn().mockResolvedValue(null), + incrementSavedObjectsBulkGet: jest.fn().mockResolvedValue(null), + incrementSavedObjectsBulkUpdate: jest.fn().mockResolvedValue(null), + incrementSavedObjectsCreate: jest.fn().mockResolvedValue(null), + incrementSavedObjectsDelete: jest.fn().mockResolvedValue(null), + incrementSavedObjectsFind: jest.fn().mockResolvedValue(null), + incrementSavedObjectsGet: jest.fn().mockResolvedValue(null), + incrementSavedObjectsUpdate: jest.fn().mockResolvedValue(null), incrementSavedObjectsImport: jest.fn().mockResolvedValue(null), incrementSavedObjectsResolveImportErrors: jest.fn().mockResolvedValue(null), incrementSavedObjectsExport: jest.fn().mockResolvedValue(null), diff --git a/src/core/server/core_usage_data/core_usage_stats_client.test.ts b/src/core/server/core_usage_data/core_usage_stats_client.test.ts index e4f47667fce6b..6b6e83f168f77 100644 --- a/src/core/server/core_usage_data/core_usage_stats_client.test.ts +++ b/src/core/server/core_usage_data/core_usage_stats_client.test.ts @@ -17,30 +17,43 @@ * under the License. */ -import { savedObjectsRepositoryMock } from '../mocks'; +import { httpServerMock, httpServiceMock, savedObjectsRepositoryMock } from '../mocks'; import { CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID } from './constants'; import { + BaseIncrementOptions, IncrementSavedObjectsImportOptions, IncrementSavedObjectsResolveImportErrorsOptions, IncrementSavedObjectsExportOptions, + BULK_CREATE_STATS_PREFIX, + BULK_GET_STATS_PREFIX, + BULK_UPDATE_STATS_PREFIX, + CREATE_STATS_PREFIX, + DELETE_STATS_PREFIX, + FIND_STATS_PREFIX, + GET_STATS_PREFIX, + UPDATE_STATS_PREFIX, IMPORT_STATS_PREFIX, RESOLVE_IMPORT_STATS_PREFIX, EXPORT_STATS_PREFIX, } from './core_usage_stats_client'; import { CoreUsageStatsClient } from '.'; +import { DEFAULT_NAMESPACE_STRING } from '../saved_objects/service/lib/utils'; describe('CoreUsageStatsClient', () => { - const setup = () => { + const setup = (namespace?: string) => { const debugLoggerMock = jest.fn(); + const basePathMock = httpServiceMock.createBasePath(); + // we could mock a return value for basePathMock.get, but it isn't necessary for testing purposes + basePathMock.remove.mockReturnValue(namespace ? `/s/${namespace}` : '/'); const repositoryMock = savedObjectsRepositoryMock.create(); const usageStatsClient = new CoreUsageStatsClient( debugLoggerMock, + basePathMock, Promise.resolve(repositoryMock) ); - return { usageStatsClient, debugLoggerMock, repositoryMock }; + return { usageStatsClient, debugLoggerMock, basePathMock, repositoryMock }; }; - - const firstPartyRequestHeaders = { 'kbn-version': 'a', origin: 'b', referer: 'c' }; // as long as these three header fields are truthy, this will be treated like a first-party request + const firstPartyRequestHeaders = { 'kbn-version': 'a', referer: 'b' }; // as long as these two header fields are truthy, this will be treated like a first-party request const incrementOptions = { refresh: false }; describe('#getUsageStats', () => { @@ -67,13 +80,616 @@ describe('CoreUsageStatsClient', () => { }); }); + describe('#incrementSavedObjectsBulkCreate', () => { + it('does not throw an error if repository incrementCounter operation fails', async () => { + const { usageStatsClient, repositoryMock } = setup(); + repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + + const request = httpServerMock.createKibanaRequest(); + await expect( + usageStatsClient.incrementSavedObjectsBulkCreate({ + request, + } as BaseIncrementOptions) + ).resolves.toBeUndefined(); + expect(repositoryMock.incrementCounter).toHaveBeenCalled(); + }); + + it('handles falsy options appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsBulkCreate({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${BULK_CREATE_STATS_PREFIX}.total`, + `${BULK_CREATE_STATS_PREFIX}.namespace.default.total`, + `${BULK_CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + ], + incrementOptions + ); + }); + + it('handles truthy options and the default namespace string appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING); + + const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }); + await usageStatsClient.incrementSavedObjectsBulkCreate({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${BULK_CREATE_STATS_PREFIX}.total`, + `${BULK_CREATE_STATS_PREFIX}.namespace.default.total`, + `${BULK_CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + ], + incrementOptions + ); + }); + + it('handles a non-default space appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup('foo'); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsBulkCreate({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${BULK_CREATE_STATS_PREFIX}.total`, + `${BULK_CREATE_STATS_PREFIX}.namespace.custom.total`, + `${BULK_CREATE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + ], + incrementOptions + ); + }); + }); + + describe('#incrementSavedObjectsBulkGet', () => { + it('does not throw an error if repository incrementCounter operation fails', async () => { + const { usageStatsClient, repositoryMock } = setup(); + repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + + const request = httpServerMock.createKibanaRequest(); + await expect( + usageStatsClient.incrementSavedObjectsBulkGet({ + request, + } as BaseIncrementOptions) + ).resolves.toBeUndefined(); + expect(repositoryMock.incrementCounter).toHaveBeenCalled(); + }); + + it('handles falsy options appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsBulkGet({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${BULK_GET_STATS_PREFIX}.total`, + `${BULK_GET_STATS_PREFIX}.namespace.default.total`, + `${BULK_GET_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + ], + incrementOptions + ); + }); + + it('handles truthy options and the default namespace string appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING); + + const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }); + await usageStatsClient.incrementSavedObjectsBulkGet({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${BULK_GET_STATS_PREFIX}.total`, + `${BULK_GET_STATS_PREFIX}.namespace.default.total`, + `${BULK_GET_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + ], + incrementOptions + ); + }); + + it('handles a non-default space appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup('foo'); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsBulkGet({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${BULK_GET_STATS_PREFIX}.total`, + `${BULK_GET_STATS_PREFIX}.namespace.custom.total`, + `${BULK_GET_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + ], + incrementOptions + ); + }); + }); + + describe('#incrementSavedObjectsBulkUpdate', () => { + it('does not throw an error if repository incrementCounter operation fails', async () => { + const { usageStatsClient, repositoryMock } = setup(); + repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + + const request = httpServerMock.createKibanaRequest(); + await expect( + usageStatsClient.incrementSavedObjectsBulkUpdate({ + request, + } as BaseIncrementOptions) + ).resolves.toBeUndefined(); + expect(repositoryMock.incrementCounter).toHaveBeenCalled(); + }); + + it('handles falsy options appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsBulkUpdate({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${BULK_UPDATE_STATS_PREFIX}.total`, + `${BULK_UPDATE_STATS_PREFIX}.namespace.default.total`, + `${BULK_UPDATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + ], + incrementOptions + ); + }); + + it('handles truthy options and the default namespace string appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING); + + const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }); + await usageStatsClient.incrementSavedObjectsBulkUpdate({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${BULK_UPDATE_STATS_PREFIX}.total`, + `${BULK_UPDATE_STATS_PREFIX}.namespace.default.total`, + `${BULK_UPDATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + ], + incrementOptions + ); + }); + + it('handles a non-default space appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup('foo'); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsBulkUpdate({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${BULK_UPDATE_STATS_PREFIX}.total`, + `${BULK_UPDATE_STATS_PREFIX}.namespace.custom.total`, + `${BULK_UPDATE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + ], + incrementOptions + ); + }); + }); + + describe('#incrementSavedObjectsCreate', () => { + it('does not throw an error if repository incrementCounter operation fails', async () => { + const { usageStatsClient, repositoryMock } = setup(); + repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + + const request = httpServerMock.createKibanaRequest(); + await expect( + usageStatsClient.incrementSavedObjectsCreate({ + request, + } as BaseIncrementOptions) + ).resolves.toBeUndefined(); + expect(repositoryMock.incrementCounter).toHaveBeenCalled(); + }); + + it('handles falsy options appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsCreate({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${CREATE_STATS_PREFIX}.total`, + `${CREATE_STATS_PREFIX}.namespace.default.total`, + `${CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + ], + incrementOptions + ); + }); + + it('handles truthy options and the default namespace string appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING); + + const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }); + await usageStatsClient.incrementSavedObjectsCreate({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${CREATE_STATS_PREFIX}.total`, + `${CREATE_STATS_PREFIX}.namespace.default.total`, + `${CREATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + ], + incrementOptions + ); + }); + + it('handles a non-default space appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup('foo'); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsCreate({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${CREATE_STATS_PREFIX}.total`, + `${CREATE_STATS_PREFIX}.namespace.custom.total`, + `${CREATE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + ], + incrementOptions + ); + }); + }); + + describe('#incrementSavedObjectsDelete', () => { + it('does not throw an error if repository incrementCounter operation fails', async () => { + const { usageStatsClient, repositoryMock } = setup(); + repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + + const request = httpServerMock.createKibanaRequest(); + await expect( + usageStatsClient.incrementSavedObjectsDelete({ + request, + } as BaseIncrementOptions) + ).resolves.toBeUndefined(); + expect(repositoryMock.incrementCounter).toHaveBeenCalled(); + }); + + it('handles falsy options appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsDelete({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${DELETE_STATS_PREFIX}.total`, + `${DELETE_STATS_PREFIX}.namespace.default.total`, + `${DELETE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + ], + incrementOptions + ); + }); + + it('handles truthy options and the default namespace string appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING); + + const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }); + await usageStatsClient.incrementSavedObjectsDelete({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${DELETE_STATS_PREFIX}.total`, + `${DELETE_STATS_PREFIX}.namespace.default.total`, + `${DELETE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + ], + incrementOptions + ); + }); + + it('handles a non-default space appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup('foo'); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsDelete({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${DELETE_STATS_PREFIX}.total`, + `${DELETE_STATS_PREFIX}.namespace.custom.total`, + `${DELETE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + ], + incrementOptions + ); + }); + }); + + describe('#incrementSavedObjectsFind', () => { + it('does not throw an error if repository incrementCounter operation fails', async () => { + const { usageStatsClient, repositoryMock } = setup(); + repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + + const request = httpServerMock.createKibanaRequest(); + await expect( + usageStatsClient.incrementSavedObjectsFind({ + request, + } as BaseIncrementOptions) + ).resolves.toBeUndefined(); + expect(repositoryMock.incrementCounter).toHaveBeenCalled(); + }); + + it('handles falsy options appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsFind({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${FIND_STATS_PREFIX}.total`, + `${FIND_STATS_PREFIX}.namespace.default.total`, + `${FIND_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + ], + incrementOptions + ); + }); + + it('handles truthy options and the default namespace string appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING); + + const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }); + await usageStatsClient.incrementSavedObjectsFind({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${FIND_STATS_PREFIX}.total`, + `${FIND_STATS_PREFIX}.namespace.default.total`, + `${FIND_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + ], + incrementOptions + ); + }); + + it('handles a non-default space appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup('foo'); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsFind({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${FIND_STATS_PREFIX}.total`, + `${FIND_STATS_PREFIX}.namespace.custom.total`, + `${FIND_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + ], + incrementOptions + ); + }); + }); + + describe('#incrementSavedObjectsGet', () => { + it('does not throw an error if repository incrementCounter operation fails', async () => { + const { usageStatsClient, repositoryMock } = setup(); + repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + + const request = httpServerMock.createKibanaRequest(); + await expect( + usageStatsClient.incrementSavedObjectsGet({ + request, + } as BaseIncrementOptions) + ).resolves.toBeUndefined(); + expect(repositoryMock.incrementCounter).toHaveBeenCalled(); + }); + + it('handles falsy options appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsGet({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${GET_STATS_PREFIX}.total`, + `${GET_STATS_PREFIX}.namespace.default.total`, + `${GET_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + ], + incrementOptions + ); + }); + + it('handles truthy options and the default namespace string appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING); + + const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }); + await usageStatsClient.incrementSavedObjectsGet({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${GET_STATS_PREFIX}.total`, + `${GET_STATS_PREFIX}.namespace.default.total`, + `${GET_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + ], + incrementOptions + ); + }); + + it('handles a non-default space appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup('foo'); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsGet({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${GET_STATS_PREFIX}.total`, + `${GET_STATS_PREFIX}.namespace.custom.total`, + `${GET_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + ], + incrementOptions + ); + }); + }); + + describe('#incrementSavedObjectsUpdate', () => { + it('does not throw an error if repository incrementCounter operation fails', async () => { + const { usageStatsClient, repositoryMock } = setup(); + repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + + const request = httpServerMock.createKibanaRequest(); + await expect( + usageStatsClient.incrementSavedObjectsUpdate({ + request, + } as BaseIncrementOptions) + ).resolves.toBeUndefined(); + expect(repositoryMock.incrementCounter).toHaveBeenCalled(); + }); + + it('handles falsy options appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsUpdate({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${UPDATE_STATS_PREFIX}.total`, + `${UPDATE_STATS_PREFIX}.namespace.default.total`, + `${UPDATE_STATS_PREFIX}.namespace.default.kibanaRequest.no`, + ], + incrementOptions + ); + }); + + it('handles truthy options and the default namespace string appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING); + + const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }); + await usageStatsClient.incrementSavedObjectsUpdate({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${UPDATE_STATS_PREFIX}.total`, + `${UPDATE_STATS_PREFIX}.namespace.default.total`, + `${UPDATE_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + ], + incrementOptions + ); + }); + + it('handles a non-default space appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup('foo'); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsUpdate({ + request, + } as BaseIncrementOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${UPDATE_STATS_PREFIX}.total`, + `${UPDATE_STATS_PREFIX}.namespace.custom.total`, + `${UPDATE_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + ], + incrementOptions + ); + }); + }); + describe('#incrementSavedObjectsImport', () => { it('does not throw an error if repository incrementCounter operation fails', async () => { const { usageStatsClient, repositoryMock } = setup(); repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + const request = httpServerMock.createKibanaRequest(); await expect( - usageStatsClient.incrementSavedObjectsImport({} as IncrementSavedObjectsImportOptions) + usageStatsClient.incrementSavedObjectsImport({ + request, + } as IncrementSavedObjectsImportOptions) ).resolves.toBeUndefined(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); }); @@ -81,14 +697,18 @@ describe('CoreUsageStatsClient', () => { it('handles falsy options appropriately', async () => { const { usageStatsClient, repositoryMock } = setup(); - await usageStatsClient.incrementSavedObjectsImport({} as IncrementSavedObjectsImportOptions); + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsImport({ + request, + } as IncrementSavedObjectsImportOptions); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ `${IMPORT_STATS_PREFIX}.total`, - `${IMPORT_STATS_PREFIX}.kibanaRequest.no`, + `${IMPORT_STATS_PREFIX}.namespace.default.total`, + `${IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.no`, `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, `${IMPORT_STATS_PREFIX}.overwriteEnabled.no`, ], @@ -96,11 +716,12 @@ describe('CoreUsageStatsClient', () => { ); }); - it('handles truthy options appropriately', async () => { - const { usageStatsClient, repositoryMock } = setup(); + it('handles truthy options and the default namespace string appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING); + const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }); await usageStatsClient.incrementSavedObjectsImport({ - headers: firstPartyRequestHeaders, + request, createNewCopies: true, overwrite: true, } as IncrementSavedObjectsImportOptions); @@ -110,13 +731,36 @@ describe('CoreUsageStatsClient', () => { CORE_USAGE_STATS_ID, [ `${IMPORT_STATS_PREFIX}.total`, - `${IMPORT_STATS_PREFIX}.kibanaRequest.yes`, + `${IMPORT_STATS_PREFIX}.namespace.default.total`, + `${IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`, `${IMPORT_STATS_PREFIX}.overwriteEnabled.yes`, ], incrementOptions ); }); + + it('handles a non-default space appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup('foo'); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsImport({ + request, + } as IncrementSavedObjectsImportOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${IMPORT_STATS_PREFIX}.total`, + `${IMPORT_STATS_PREFIX}.namespace.custom.total`, + `${IMPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, + `${IMPORT_STATS_PREFIX}.overwriteEnabled.no`, + ], + incrementOptions + ); + }); }); describe('#incrementSavedObjectsResolveImportErrors', () => { @@ -124,10 +768,11 @@ describe('CoreUsageStatsClient', () => { const { usageStatsClient, repositoryMock } = setup(); repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + const request = httpServerMock.createKibanaRequest(); await expect( - usageStatsClient.incrementSavedObjectsResolveImportErrors( - {} as IncrementSavedObjectsResolveImportErrorsOptions - ) + usageStatsClient.incrementSavedObjectsResolveImportErrors({ + request, + } as IncrementSavedObjectsResolveImportErrorsOptions) ).resolves.toBeUndefined(); expect(repositoryMock.incrementCounter).toHaveBeenCalled(); }); @@ -135,27 +780,30 @@ describe('CoreUsageStatsClient', () => { it('handles falsy options appropriately', async () => { const { usageStatsClient, repositoryMock } = setup(); - await usageStatsClient.incrementSavedObjectsResolveImportErrors( - {} as IncrementSavedObjectsResolveImportErrorsOptions - ); + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsResolveImportErrors({ + request, + } as IncrementSavedObjectsResolveImportErrorsOptions); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ `${RESOLVE_IMPORT_STATS_PREFIX}.total`, - `${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.no`, + `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.total`, + `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.no`, `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, ], incrementOptions ); }); - it('handles truthy options appropriately', async () => { - const { usageStatsClient, repositoryMock } = setup(); + it('handles truthy options and the default namespace string appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING); + const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }); await usageStatsClient.incrementSavedObjectsResolveImportErrors({ - headers: firstPartyRequestHeaders, + request, createNewCopies: true, } as IncrementSavedObjectsResolveImportErrorsOptions); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); @@ -164,12 +812,34 @@ describe('CoreUsageStatsClient', () => { CORE_USAGE_STATS_ID, [ `${RESOLVE_IMPORT_STATS_PREFIX}.total`, - `${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.yes`, + `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.total`, + `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`, ], incrementOptions ); }); + + it('handles a non-default space appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup('foo'); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsResolveImportErrors({ + request, + } as IncrementSavedObjectsResolveImportErrorsOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${RESOLVE_IMPORT_STATS_PREFIX}.total`, + `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.custom.total`, + `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, + ], + incrementOptions + ); + }); }); describe('#incrementSavedObjectsExport', () => { @@ -177,8 +847,11 @@ describe('CoreUsageStatsClient', () => { const { usageStatsClient, repositoryMock } = setup(); repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + const request = httpServerMock.createKibanaRequest(); await expect( - usageStatsClient.incrementSavedObjectsExport({} as IncrementSavedObjectsExportOptions) + usageStatsClient.incrementSavedObjectsExport({ + request, + } as IncrementSavedObjectsExportOptions) ).resolves.toBeUndefined(); expect(repositoryMock.incrementCounter).toHaveBeenCalled(); }); @@ -186,7 +859,9 @@ describe('CoreUsageStatsClient', () => { it('handles falsy options appropriately', async () => { const { usageStatsClient, repositoryMock } = setup(); + const request = httpServerMock.createKibanaRequest(); await usageStatsClient.incrementSavedObjectsExport({ + request, types: undefined, supportedTypes: ['foo', 'bar'], } as IncrementSavedObjectsExportOptions); @@ -196,18 +871,20 @@ describe('CoreUsageStatsClient', () => { CORE_USAGE_STATS_ID, [ `${EXPORT_STATS_PREFIX}.total`, - `${EXPORT_STATS_PREFIX}.kibanaRequest.no`, + `${EXPORT_STATS_PREFIX}.namespace.default.total`, + `${EXPORT_STATS_PREFIX}.namespace.default.kibanaRequest.no`, `${EXPORT_STATS_PREFIX}.allTypesSelected.no`, ], incrementOptions ); }); - it('handles truthy options appropriately', async () => { - const { usageStatsClient, repositoryMock } = setup(); + it('handles truthy options and the default namespace string appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING); + const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }); await usageStatsClient.incrementSavedObjectsExport({ - headers: firstPartyRequestHeaders, + request, types: ['foo', 'bar'], supportedTypes: ['foo', 'bar'], } as IncrementSavedObjectsExportOptions); @@ -217,11 +894,33 @@ describe('CoreUsageStatsClient', () => { CORE_USAGE_STATS_ID, [ `${EXPORT_STATS_PREFIX}.total`, - `${EXPORT_STATS_PREFIX}.kibanaRequest.yes`, + `${EXPORT_STATS_PREFIX}.namespace.default.total`, + `${EXPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, `${EXPORT_STATS_PREFIX}.allTypesSelected.yes`, ], incrementOptions ); }); + + it('handles a non-default space appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup('foo'); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsExport({ + request, + } as IncrementSavedObjectsExportOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${EXPORT_STATS_PREFIX}.total`, + `${EXPORT_STATS_PREFIX}.namespace.custom.total`, + `${EXPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + `${EXPORT_STATS_PREFIX}.allTypesSelected.no`, + ], + incrementOptions + ); + }); }); }); diff --git a/src/core/server/core_usage_data/core_usage_stats_client.ts b/src/core/server/core_usage_data/core_usage_stats_client.ts index 58356832d8b8a..c8d48597fae88 100644 --- a/src/core/server/core_usage_data/core_usage_stats_client.ts +++ b/src/core/server/core_usage_data/core_usage_stats_client.ts @@ -19,16 +19,19 @@ import { CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID } from './constants'; import { CoreUsageStats } from './types'; +import { DEFAULT_NAMESPACE_STRING } from '../saved_objects/service/lib/utils'; import { - Headers, ISavedObjectsRepository, SavedObjectsImportOptions, SavedObjectsResolveImportErrorsOptions, SavedObjectsExportOptions, + KibanaRequest, + IBasePath, } from '..'; -interface BaseIncrementOptions { - headers?: Headers; +/** @internal */ +export interface BaseIncrementOptions { + request: KibanaRequest; } /** @internal */ export type IncrementSavedObjectsImportOptions = BaseIncrementOptions & @@ -40,33 +43,47 @@ export type IncrementSavedObjectsResolveImportErrorsOptions = BaseIncrementOptio export type IncrementSavedObjectsExportOptions = BaseIncrementOptions & Pick & { supportedTypes: string[] }; +export const BULK_CREATE_STATS_PREFIX = 'apiCalls.savedObjectsBulkCreate'; +export const BULK_GET_STATS_PREFIX = 'apiCalls.savedObjectsBulkGet'; +export const BULK_UPDATE_STATS_PREFIX = 'apiCalls.savedObjectsBulkUpdate'; +export const CREATE_STATS_PREFIX = 'apiCalls.savedObjectsCreate'; +export const DELETE_STATS_PREFIX = 'apiCalls.savedObjectsDelete'; +export const FIND_STATS_PREFIX = 'apiCalls.savedObjectsFind'; +export const GET_STATS_PREFIX = 'apiCalls.savedObjectsGet'; +export const UPDATE_STATS_PREFIX = 'apiCalls.savedObjectsUpdate'; export const IMPORT_STATS_PREFIX = 'apiCalls.savedObjectsImport'; export const RESOLVE_IMPORT_STATS_PREFIX = 'apiCalls.savedObjectsResolveImportErrors'; export const EXPORT_STATS_PREFIX = 'apiCalls.savedObjectsExport'; const ALL_COUNTER_FIELDS = [ - `${IMPORT_STATS_PREFIX}.total`, - `${IMPORT_STATS_PREFIX}.kibanaRequest.yes`, - `${IMPORT_STATS_PREFIX}.kibanaRequest.no`, + // Saved Objects Client APIs + ...getFieldsForCounter(BULK_CREATE_STATS_PREFIX), + ...getFieldsForCounter(BULK_GET_STATS_PREFIX), + ...getFieldsForCounter(BULK_UPDATE_STATS_PREFIX), + ...getFieldsForCounter(CREATE_STATS_PREFIX), + ...getFieldsForCounter(DELETE_STATS_PREFIX), + ...getFieldsForCounter(FIND_STATS_PREFIX), + ...getFieldsForCounter(GET_STATS_PREFIX), + ...getFieldsForCounter(UPDATE_STATS_PREFIX), + // Saved Objects Management APIs + ...getFieldsForCounter(IMPORT_STATS_PREFIX), `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`, `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, `${IMPORT_STATS_PREFIX}.overwriteEnabled.yes`, `${IMPORT_STATS_PREFIX}.overwriteEnabled.no`, - `${RESOLVE_IMPORT_STATS_PREFIX}.total`, - `${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.yes`, - `${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.no`, + ...getFieldsForCounter(RESOLVE_IMPORT_STATS_PREFIX), `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`, `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, - `${EXPORT_STATS_PREFIX}.total`, - `${EXPORT_STATS_PREFIX}.kibanaRequest.yes`, - `${EXPORT_STATS_PREFIX}.kibanaRequest.no`, + ...getFieldsForCounter(EXPORT_STATS_PREFIX), `${EXPORT_STATS_PREFIX}.allTypesSelected.yes`, `${EXPORT_STATS_PREFIX}.allTypesSelected.no`, ]; +const SPACE_CONTEXT_REGEX = /^\/s\/([a-z0-9_\-]+)/; /** @internal */ export class CoreUsageStatsClient { constructor( private readonly debugLogger: (message: string) => void, + private readonly basePath: IBasePath, private readonly repositoryPromise: Promise ) {} @@ -88,67 +105,128 @@ export class CoreUsageStatsClient { return coreUsageStats; } - public async incrementSavedObjectsImport({ - headers, - createNewCopies, - overwrite, - }: IncrementSavedObjectsImportOptions) { - const isKibanaRequest = getIsKibanaRequest(headers); + public async incrementSavedObjectsBulkCreate(options: BaseIncrementOptions) { + await this.updateUsageStats([], BULK_CREATE_STATS_PREFIX, options); + } + + public async incrementSavedObjectsBulkGet(options: BaseIncrementOptions) { + await this.updateUsageStats([], BULK_GET_STATS_PREFIX, options); + } + + public async incrementSavedObjectsBulkUpdate(options: BaseIncrementOptions) { + await this.updateUsageStats([], BULK_UPDATE_STATS_PREFIX, options); + } + + public async incrementSavedObjectsCreate(options: BaseIncrementOptions) { + await this.updateUsageStats([], CREATE_STATS_PREFIX, options); + } + + public async incrementSavedObjectsDelete(options: BaseIncrementOptions) { + await this.updateUsageStats([], DELETE_STATS_PREFIX, options); + } + + public async incrementSavedObjectsFind(options: BaseIncrementOptions) { + await this.updateUsageStats([], FIND_STATS_PREFIX, options); + } + + public async incrementSavedObjectsGet(options: BaseIncrementOptions) { + await this.updateUsageStats([], GET_STATS_PREFIX, options); + } + + public async incrementSavedObjectsUpdate(options: BaseIncrementOptions) { + await this.updateUsageStats([], UPDATE_STATS_PREFIX, options); + } + + public async incrementSavedObjectsImport(options: IncrementSavedObjectsImportOptions) { + const { createNewCopies, overwrite } = options; const counterFieldNames = [ - 'total', - `kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`, `createNewCopiesEnabled.${createNewCopies ? 'yes' : 'no'}`, `overwriteEnabled.${overwrite ? 'yes' : 'no'}`, ]; - await this.updateUsageStats(counterFieldNames, IMPORT_STATS_PREFIX); + await this.updateUsageStats(counterFieldNames, IMPORT_STATS_PREFIX, options); } - public async incrementSavedObjectsResolveImportErrors({ - headers, - createNewCopies, - }: IncrementSavedObjectsResolveImportErrorsOptions) { - const isKibanaRequest = getIsKibanaRequest(headers); - const counterFieldNames = [ - 'total', - `kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`, - `createNewCopiesEnabled.${createNewCopies ? 'yes' : 'no'}`, - ]; - await this.updateUsageStats(counterFieldNames, RESOLVE_IMPORT_STATS_PREFIX); + public async incrementSavedObjectsResolveImportErrors( + options: IncrementSavedObjectsResolveImportErrorsOptions + ) { + const { createNewCopies } = options; + const counterFieldNames = [`createNewCopiesEnabled.${createNewCopies ? 'yes' : 'no'}`]; + await this.updateUsageStats(counterFieldNames, RESOLVE_IMPORT_STATS_PREFIX, options); } - public async incrementSavedObjectsExport({ - headers, - types, - supportedTypes, - }: IncrementSavedObjectsExportOptions) { - const isKibanaRequest = getIsKibanaRequest(headers); + public async incrementSavedObjectsExport(options: IncrementSavedObjectsExportOptions) { + const { types, supportedTypes } = options; const isAllTypesSelected = !!types && supportedTypes.every((x) => types.includes(x)); - const counterFieldNames = [ - 'total', - `kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`, - `allTypesSelected.${isAllTypesSelected ? 'yes' : 'no'}`, - ]; - await this.updateUsageStats(counterFieldNames, EXPORT_STATS_PREFIX); + const counterFieldNames = [`allTypesSelected.${isAllTypesSelected ? 'yes' : 'no'}`]; + await this.updateUsageStats(counterFieldNames, EXPORT_STATS_PREFIX, options); } - private async updateUsageStats(counterFieldNames: string[], prefix: string) { + private async updateUsageStats( + counterFieldNames: string[], + prefix: string, + { request }: BaseIncrementOptions + ) { const options = { refresh: false }; try { const repository = await this.repositoryPromise; + const fields = this.getFieldsToIncrement(counterFieldNames, prefix, request); await repository.incrementCounter( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, - counterFieldNames.map((x) => `${prefix}.${x}`), + fields, options ); } catch (err) { // do nothing } } + + private getIsDefaultNamespace(request: KibanaRequest) { + const requestBasePath = this.basePath.get(request); // obtain the original request basePath, as it may have been modified by a request interceptor + const pathToCheck = this.basePath.remove(requestBasePath); // remove the server basePath from the request basePath + const matchResult = pathToCheck.match(SPACE_CONTEXT_REGEX); // Look for `/s/space-url-context` in the base path + + if (!matchResult || matchResult.length === 0) { + return true; + } + + // Ignoring first result, we only want the capture group result at index 1 + const [, spaceId] = matchResult; + + return spaceId === DEFAULT_NAMESPACE_STRING; + } + + private getFieldsToIncrement( + counterFieldNames: string[], + prefix: string, + request: KibanaRequest + ) { + const isKibanaRequest = getIsKibanaRequest(request); + const isDefaultNamespace = this.getIsDefaultNamespace(request); + const namespaceField = isDefaultNamespace ? 'default' : 'custom'; + return [ + 'total', + `namespace.${namespaceField}.total`, + `namespace.${namespaceField}.kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`, + ...counterFieldNames, + ].map((x) => `${prefix}.${x}`); + } +} + +function getFieldsForCounter(prefix: string) { + return [ + 'total', + 'namespace.default.total', + 'namespace.default.kibanaRequest.yes', + 'namespace.default.kibanaRequest.no', + 'namespace.custom.total', + 'namespace.custom.kibanaRequest.yes', + 'namespace.custom.kibanaRequest.no', + ].map((x) => `${prefix}.${x}`); } -function getIsKibanaRequest(headers?: Headers) { - // The presence of these three request headers gives us a good indication that this is a first-party request from the Kibana client. +function getIsKibanaRequest({ headers }: KibanaRequest) { + // The presence of these two request headers gives us a good indication that this is a first-party request from the Kibana client. // We can't be 100% certain, but this is a reasonable attempt. - return headers && headers['kbn-version'] && headers.origin && headers.referer; + return headers && headers['kbn-version'] && headers.referer; } diff --git a/src/core/server/core_usage_data/types.ts b/src/core/server/core_usage_data/types.ts index aa41d75e6f2d4..b7952334b4be4 100644 --- a/src/core/server/core_usage_data/types.ts +++ b/src/core/server/core_usage_data/types.ts @@ -27,21 +27,91 @@ import { ISavedObjectTypeRegistry, SavedObjectTypeRegistry } from '..'; * includes point-in-time configuration information. * */ export interface CoreUsageStats { + // Saved Objects Client APIs + 'apiCalls.savedObjectsBulkCreate.total'?: number; + 'apiCalls.savedObjectsBulkCreate.namespace.default.total'?: number; + 'apiCalls.savedObjectsBulkCreate.namespace.default.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsBulkCreate.namespace.default.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsBulkCreate.namespace.custom.total'?: number; + 'apiCalls.savedObjectsBulkCreate.namespace.custom.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsBulkCreate.namespace.custom.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsBulkGet.total'?: number; + 'apiCalls.savedObjectsBulkGet.namespace.default.total'?: number; + 'apiCalls.savedObjectsBulkGet.namespace.default.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsBulkGet.namespace.default.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsBulkGet.namespace.custom.total'?: number; + 'apiCalls.savedObjectsBulkGet.namespace.custom.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsBulkGet.namespace.custom.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsBulkUpdate.total'?: number; + 'apiCalls.savedObjectsBulkUpdate.namespace.default.total'?: number; + 'apiCalls.savedObjectsBulkUpdate.namespace.default.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsBulkUpdate.namespace.default.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsBulkUpdate.namespace.custom.total'?: number; + 'apiCalls.savedObjectsBulkUpdate.namespace.custom.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsBulkUpdate.namespace.custom.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsCreate.total'?: number; + 'apiCalls.savedObjectsCreate.namespace.default.total'?: number; + 'apiCalls.savedObjectsCreate.namespace.default.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsCreate.namespace.default.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsCreate.namespace.custom.total'?: number; + 'apiCalls.savedObjectsCreate.namespace.custom.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsCreate.namespace.custom.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsDelete.total'?: number; + 'apiCalls.savedObjectsDelete.namespace.default.total'?: number; + 'apiCalls.savedObjectsDelete.namespace.default.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsDelete.namespace.default.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsDelete.namespace.custom.total'?: number; + 'apiCalls.savedObjectsDelete.namespace.custom.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsDelete.namespace.custom.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsFind.total'?: number; + 'apiCalls.savedObjectsFind.namespace.default.total'?: number; + 'apiCalls.savedObjectsFind.namespace.default.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsFind.namespace.default.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsFind.namespace.custom.total'?: number; + 'apiCalls.savedObjectsFind.namespace.custom.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsFind.namespace.custom.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsGet.total'?: number; + 'apiCalls.savedObjectsGet.namespace.default.total'?: number; + 'apiCalls.savedObjectsGet.namespace.default.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsGet.namespace.default.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsGet.namespace.custom.total'?: number; + 'apiCalls.savedObjectsGet.namespace.custom.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsGet.namespace.custom.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsUpdate.total'?: number; + 'apiCalls.savedObjectsUpdate.namespace.default.total'?: number; + 'apiCalls.savedObjectsUpdate.namespace.default.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsUpdate.namespace.default.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsUpdate.namespace.custom.total'?: number; + 'apiCalls.savedObjectsUpdate.namespace.custom.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsUpdate.namespace.custom.kibanaRequest.no'?: number; + // Saved Objects Management APIs 'apiCalls.savedObjectsImport.total'?: number; - 'apiCalls.savedObjectsImport.kibanaRequest.yes'?: number; - 'apiCalls.savedObjectsImport.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsImport.namespace.default.total'?: number; + 'apiCalls.savedObjectsImport.namespace.default.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsImport.namespace.default.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsImport.namespace.custom.total'?: number; + 'apiCalls.savedObjectsImport.namespace.custom.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsImport.namespace.custom.kibanaRequest.no'?: number; 'apiCalls.savedObjectsImport.createNewCopiesEnabled.yes'?: number; 'apiCalls.savedObjectsImport.createNewCopiesEnabled.no'?: number; 'apiCalls.savedObjectsImport.overwriteEnabled.yes'?: number; 'apiCalls.savedObjectsImport.overwriteEnabled.no'?: number; 'apiCalls.savedObjectsResolveImportErrors.total'?: number; - 'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.yes'?: number; - 'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsResolveImportErrors.namespace.default.total'?: number; + 'apiCalls.savedObjectsResolveImportErrors.namespace.default.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsResolveImportErrors.namespace.default.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsResolveImportErrors.namespace.custom.total'?: number; + 'apiCalls.savedObjectsResolveImportErrors.namespace.custom.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsResolveImportErrors.namespace.custom.kibanaRequest.no'?: number; 'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.yes'?: number; 'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.no'?: number; 'apiCalls.savedObjectsExport.total'?: number; - 'apiCalls.savedObjectsExport.kibanaRequest.yes'?: number; - 'apiCalls.savedObjectsExport.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsExport.namespace.default.total'?: number; + 'apiCalls.savedObjectsExport.namespace.default.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsExport.namespace.default.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsExport.namespace.custom.total'?: number; + 'apiCalls.savedObjectsExport.namespace.custom.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsExport.namespace.custom.kibanaRequest.no'?: number; 'apiCalls.savedObjectsExport.allTypesSelected.yes'?: number; 'apiCalls.savedObjectsExport.allTypesSelected.no'?: number; } diff --git a/src/core/server/saved_objects/routes/bulk_create.ts b/src/core/server/saved_objects/routes/bulk_create.ts index b048c5d8f99bf..b1286f3a1f06c 100644 --- a/src/core/server/saved_objects/routes/bulk_create.ts +++ b/src/core/server/saved_objects/routes/bulk_create.ts @@ -19,8 +19,13 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; +import { CoreUsageDataSetup } from '../../core_usage_data'; -export const registerBulkCreateRoute = (router: IRouter) => { +interface RouteDependencies { + coreUsageData: CoreUsageDataSetup; +} + +export const registerBulkCreateRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { router.post( { path: '/_bulk_create', @@ -51,6 +56,10 @@ export const registerBulkCreateRoute = (router: IRouter) => { }, router.handleLegacyErrors(async (context, req, res) => { const { overwrite } = req.query; + + const usageStatsClient = coreUsageData.getClient(); + usageStatsClient.incrementSavedObjectsBulkCreate({ request: req }).catch(() => {}); + const result = await context.core.savedObjects.client.bulkCreate(req.body, { overwrite }); return res.ok({ body: result }); }) diff --git a/src/core/server/saved_objects/routes/bulk_get.ts b/src/core/server/saved_objects/routes/bulk_get.ts index 067388dcf9220..41c77520b4faf 100644 --- a/src/core/server/saved_objects/routes/bulk_get.ts +++ b/src/core/server/saved_objects/routes/bulk_get.ts @@ -19,8 +19,13 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; +import { CoreUsageDataSetup } from '../../core_usage_data'; -export const registerBulkGetRoute = (router: IRouter) => { +interface RouteDependencies { + coreUsageData: CoreUsageDataSetup; +} + +export const registerBulkGetRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { router.post( { path: '/_bulk_get', @@ -35,6 +40,9 @@ export const registerBulkGetRoute = (router: IRouter) => { }, }, router.handleLegacyErrors(async (context, req, res) => { + const usageStatsClient = coreUsageData.getClient(); + usageStatsClient.incrementSavedObjectsBulkGet({ request: req }).catch(() => {}); + const result = await context.core.savedObjects.client.bulkGet(req.body); return res.ok({ body: result }); }) diff --git a/src/core/server/saved_objects/routes/bulk_update.ts b/src/core/server/saved_objects/routes/bulk_update.ts index 882213644146a..b4014b5422d5d 100644 --- a/src/core/server/saved_objects/routes/bulk_update.ts +++ b/src/core/server/saved_objects/routes/bulk_update.ts @@ -19,8 +19,13 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; +import { CoreUsageDataSetup } from '../../core_usage_data'; -export const registerBulkUpdateRoute = (router: IRouter) => { +interface RouteDependencies { + coreUsageData: CoreUsageDataSetup; +} + +export const registerBulkUpdateRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { router.put( { path: '/_bulk_update', @@ -46,6 +51,9 @@ export const registerBulkUpdateRoute = (router: IRouter) => { }, }, router.handleLegacyErrors(async (context, req, res) => { + const usageStatsClient = coreUsageData.getClient(); + usageStatsClient.incrementSavedObjectsBulkUpdate({ request: req }).catch(() => {}); + const savedObject = await context.core.savedObjects.client.bulkUpdate(req.body); return res.ok({ body: savedObject }); }) diff --git a/src/core/server/saved_objects/routes/create.ts b/src/core/server/saved_objects/routes/create.ts index 816315705a375..cb6a849be9f2d 100644 --- a/src/core/server/saved_objects/routes/create.ts +++ b/src/core/server/saved_objects/routes/create.ts @@ -19,8 +19,13 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; +import { CoreUsageDataSetup } from '../../core_usage_data'; -export const registerCreateRoute = (router: IRouter) => { +interface RouteDependencies { + coreUsageData: CoreUsageDataSetup; +} + +export const registerCreateRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { router.post( { path: '/{type}/{id?}', @@ -53,6 +58,9 @@ export const registerCreateRoute = (router: IRouter) => { const { overwrite } = req.query; const { attributes, migrationVersion, references, initialNamespaces } = req.body; + const usageStatsClient = coreUsageData.getClient(); + usageStatsClient.incrementSavedObjectsCreate({ request: req }).catch(() => {}); + const options = { id, overwrite, migrationVersion, references, initialNamespaces }; const result = await context.core.savedObjects.client.create(type, attributes, options); return res.ok({ body: result }); diff --git a/src/core/server/saved_objects/routes/delete.ts b/src/core/server/saved_objects/routes/delete.ts index d99397d2a050c..69d2290325a93 100644 --- a/src/core/server/saved_objects/routes/delete.ts +++ b/src/core/server/saved_objects/routes/delete.ts @@ -19,8 +19,13 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; +import { CoreUsageDataSetup } from '../../core_usage_data'; -export const registerDeleteRoute = (router: IRouter) => { +interface RouteDependencies { + coreUsageData: CoreUsageDataSetup; +} + +export const registerDeleteRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { router.delete( { path: '/{type}/{id}', @@ -37,6 +42,10 @@ export const registerDeleteRoute = (router: IRouter) => { router.handleLegacyErrors(async (context, req, res) => { const { type, id } = req.params; const { force } = req.query; + + const usageStatsClient = coreUsageData.getClient(); + usageStatsClient.incrementSavedObjectsDelete({ request: req }).catch(() => {}); + const result = await context.core.savedObjects.client.delete(type, id, { force }); return res.ok({ body: result }); }) diff --git a/src/core/server/saved_objects/routes/export.ts b/src/core/server/saved_objects/routes/export.ts index 387280d777eaa..8f5c19d927d40 100644 --- a/src/core/server/saved_objects/routes/export.ts +++ b/src/core/server/saved_objects/routes/export.ts @@ -104,10 +104,9 @@ export const registerExportRoute = ( } } - const { headers } = req; const usageStatsClient = coreUsageData.getClient(); usageStatsClient - .incrementSavedObjectsExport({ headers, types, supportedTypes }) + .incrementSavedObjectsExport({ request: req, types, supportedTypes }) .catch(() => {}); const exportStream = await exportSavedObjectsToStream({ diff --git a/src/core/server/saved_objects/routes/find.ts b/src/core/server/saved_objects/routes/find.ts index 915d0cccf7af9..7ddcfa91da22d 100644 --- a/src/core/server/saved_objects/routes/find.ts +++ b/src/core/server/saved_objects/routes/find.ts @@ -19,8 +19,13 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; +import { CoreUsageDataSetup } from '../../core_usage_data'; -export const registerFindRoute = (router: IRouter) => { +interface RouteDependencies { + coreUsageData: CoreUsageDataSetup; +} + +export const registerFindRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { const referenceSchema = schema.object({ type: schema.string(), id: schema.string(), @@ -61,6 +66,9 @@ export const registerFindRoute = (router: IRouter) => { const namespaces = typeof req.query.namespaces === 'string' ? [req.query.namespaces] : req.query.namespaces; + const usageStatsClient = coreUsageData.getClient(); + usageStatsClient.incrementSavedObjectsFind({ request: req }).catch(() => {}); + const result = await context.core.savedObjects.client.find({ perPage: query.per_page, page: query.page, diff --git a/src/core/server/saved_objects/routes/get.ts b/src/core/server/saved_objects/routes/get.ts index f1b974c70b1a9..d29229eab33ff 100644 --- a/src/core/server/saved_objects/routes/get.ts +++ b/src/core/server/saved_objects/routes/get.ts @@ -19,8 +19,13 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; +import { CoreUsageDataSetup } from '../../core_usage_data'; -export const registerGetRoute = (router: IRouter) => { +interface RouteDependencies { + coreUsageData: CoreUsageDataSetup; +} + +export const registerGetRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { router.get( { path: '/{type}/{id}', @@ -33,6 +38,10 @@ export const registerGetRoute = (router: IRouter) => { }, router.handleLegacyErrors(async (context, req, res) => { const { type, id } = req.params; + + const usageStatsClient = coreUsageData.getClient(); + usageStatsClient.incrementSavedObjectsGet({ request: req }).catch(() => {}); + const savedObject = await context.core.savedObjects.client.get(type, id); return res.ok({ body: savedObject }); }) diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index 27be710c0a92a..ebc52c32e2c70 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -75,10 +75,9 @@ export const registerImportRoute = ( router.handleLegacyErrors(async (context, req, res) => { const { overwrite, createNewCopies } = req.query; - const { headers } = req; const usageStatsClient = coreUsageData.getClient(); usageStatsClient - .incrementSavedObjectsImport({ headers, createNewCopies, overwrite }) + .incrementSavedObjectsImport({ request: req, createNewCopies, overwrite }) .catch(() => {}); const file = req.body.file as FileStream; diff --git a/src/core/server/saved_objects/routes/index.ts b/src/core/server/saved_objects/routes/index.ts index 19154b8583654..0ffd1104d35e2 100644 --- a/src/core/server/saved_objects/routes/index.ts +++ b/src/core/server/saved_objects/routes/index.ts @@ -51,14 +51,14 @@ export function registerRoutes({ }) { const router = http.createRouter('/api/saved_objects/'); - registerGetRoute(router); - registerCreateRoute(router); - registerDeleteRoute(router); - registerFindRoute(router); - registerUpdateRoute(router); - registerBulkGetRoute(router); - registerBulkCreateRoute(router); - registerBulkUpdateRoute(router); + registerGetRoute(router, { coreUsageData }); + registerCreateRoute(router, { coreUsageData }); + registerDeleteRoute(router, { coreUsageData }); + registerFindRoute(router, { coreUsageData }); + registerUpdateRoute(router, { coreUsageData }); + registerBulkGetRoute(router, { coreUsageData }); + registerBulkCreateRoute(router, { coreUsageData }); + registerBulkUpdateRoute(router, { coreUsageData }); registerLogLegacyImportRoute(router, logger); registerExportRoute(router, { config, coreUsageData }); registerImportRoute(router, { config, coreUsageData }); diff --git a/src/core/server/saved_objects/routes/integration_tests/bulk_create.test.ts b/src/core/server/saved_objects/routes/integration_tests/bulk_create.test.ts index 3d455ff9d594c..186b21ef361a9 100644 --- a/src/core/server/saved_objects/routes/integration_tests/bulk_create.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/bulk_create.test.ts @@ -21,6 +21,9 @@ import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerBulkCreateRoute } from '../bulk_create'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; +import { CoreUsageStatsClient } from '../../../core_usage_data'; +import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock'; +import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock'; import { setupServer } from '../test_utils'; type SetupServerReturn = UnwrapPromise>; @@ -30,6 +33,7 @@ describe('POST /api/saved_objects/_bulk_create', () => { let httpSetup: SetupServerReturn['httpSetup']; let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; + let coreUsageStatsClient: jest.Mocked; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); @@ -37,7 +41,10 @@ describe('POST /api/saved_objects/_bulk_create', () => { savedObjectsClient.bulkCreate.mockResolvedValue({ saved_objects: [] }); const router = httpSetup.createRouter('/api/saved_objects/'); - registerBulkCreateRoute(router); + coreUsageStatsClient = coreUsageStatsClientMock.create(); + coreUsageStatsClient.incrementSavedObjectsBulkCreate.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail + const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); + registerBulkCreateRoute(router, { coreUsageData }); await server.start(); }); @@ -46,7 +53,7 @@ describe('POST /api/saved_objects/_bulk_create', () => { await server.stop(); }); - it('formats successful response', async () => { + it('formats successful response and records usage stats', async () => { const clientResponse = { saved_objects: [ { @@ -75,6 +82,9 @@ describe('POST /api/saved_objects/_bulk_create', () => { .expect(200); expect(result.body).toEqual(clientResponse); + expect(coreUsageStatsClient.incrementSavedObjectsBulkCreate).toHaveBeenCalledWith({ + request: expect.anything(), + }); }); it('calls upon savedObjectClient.bulkCreate', async () => { diff --git a/src/core/server/saved_objects/routes/integration_tests/bulk_get.test.ts b/src/core/server/saved_objects/routes/integration_tests/bulk_get.test.ts index 5deea94299d7d..c6028f86fcc7c 100644 --- a/src/core/server/saved_objects/routes/integration_tests/bulk_get.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/bulk_get.test.ts @@ -21,6 +21,9 @@ import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerBulkGetRoute } from '../bulk_get'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; +import { CoreUsageStatsClient } from '../../../core_usage_data'; +import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock'; +import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock'; import { setupServer } from '../test_utils'; type SetupServerReturn = UnwrapPromise>; @@ -30,6 +33,7 @@ describe('POST /api/saved_objects/_bulk_get', () => { let httpSetup: SetupServerReturn['httpSetup']; let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; + let coreUsageStatsClient: jest.Mocked; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); @@ -39,7 +43,10 @@ describe('POST /api/saved_objects/_bulk_get', () => { saved_objects: [], }); const router = httpSetup.createRouter('/api/saved_objects/'); - registerBulkGetRoute(router); + coreUsageStatsClient = coreUsageStatsClientMock.create(); + coreUsageStatsClient.incrementSavedObjectsBulkGet.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail + const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); + registerBulkGetRoute(router, { coreUsageData }); await server.start(); }); @@ -48,7 +55,7 @@ describe('POST /api/saved_objects/_bulk_get', () => { await server.stop(); }); - it('formats successful response', async () => { + it('formats successful response and records usage stats', async () => { const clientResponse = { saved_objects: [ { @@ -74,6 +81,9 @@ describe('POST /api/saved_objects/_bulk_get', () => { .expect(200); expect(result.body).toEqual(clientResponse); + expect(coreUsageStatsClient.incrementSavedObjectsBulkGet).toHaveBeenCalledWith({ + request: expect.anything(), + }); }); it('calls upon savedObjectClient.bulkGet', async () => { diff --git a/src/core/server/saved_objects/routes/integration_tests/bulk_update.test.ts b/src/core/server/saved_objects/routes/integration_tests/bulk_update.test.ts index 45f310ecc3fa2..c038c5303dd69 100644 --- a/src/core/server/saved_objects/routes/integration_tests/bulk_update.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/bulk_update.test.ts @@ -21,6 +21,9 @@ import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerBulkUpdateRoute } from '../bulk_update'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; +import { CoreUsageStatsClient } from '../../../core_usage_data'; +import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock'; +import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock'; import { setupServer } from '../test_utils'; type SetupServerReturn = UnwrapPromise>; @@ -30,13 +33,17 @@ describe('PUT /api/saved_objects/_bulk_update', () => { let httpSetup: SetupServerReturn['httpSetup']; let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; + let coreUsageStatsClient: jest.Mocked; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); savedObjectsClient = handlerContext.savedObjects.client; const router = httpSetup.createRouter('/api/saved_objects/'); - registerBulkUpdateRoute(router); + coreUsageStatsClient = coreUsageStatsClientMock.create(); + coreUsageStatsClient.incrementSavedObjectsBulkUpdate.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail + const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); + registerBulkUpdateRoute(router, { coreUsageData }); await server.start(); }); @@ -45,7 +52,7 @@ describe('PUT /api/saved_objects/_bulk_update', () => { await server.stop(); }); - it('formats successful response', async () => { + it('formats successful response and records usage stats', async () => { const time = Date.now().toLocaleString(); const clientResponse = [ { @@ -92,6 +99,9 @@ describe('PUT /api/saved_objects/_bulk_update', () => { .expect(200); expect(result.body).toEqual({ saved_objects: clientResponse }); + expect(coreUsageStatsClient.incrementSavedObjectsBulkUpdate).toHaveBeenCalledWith({ + request: expect.anything(), + }); }); it('calls upon savedObjectClient.bulkUpdate', async () => { diff --git a/src/core/server/saved_objects/routes/integration_tests/create.test.ts b/src/core/server/saved_objects/routes/integration_tests/create.test.ts index 9e69c3dbc64ec..8c209a05f2948 100644 --- a/src/core/server/saved_objects/routes/integration_tests/create.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/create.test.ts @@ -21,6 +21,9 @@ import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerCreateRoute } from '../create'; import { savedObjectsClientMock } from '../../service/saved_objects_client.mock'; +import { CoreUsageStatsClient } from '../../../core_usage_data'; +import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock'; +import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock'; import { setupServer } from '../test_utils'; type SetupServerReturn = UnwrapPromise>; @@ -30,6 +33,7 @@ describe('POST /api/saved_objects/{type}', () => { let httpSetup: SetupServerReturn['httpSetup']; let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; + let coreUsageStatsClient: jest.Mocked; const clientResponse = { id: 'logstash-*', @@ -46,7 +50,10 @@ describe('POST /api/saved_objects/{type}', () => { savedObjectsClient.create.mockImplementation(() => Promise.resolve(clientResponse)); const router = httpSetup.createRouter('/api/saved_objects/'); - registerCreateRoute(router); + coreUsageStatsClient = coreUsageStatsClientMock.create(); + coreUsageStatsClient.incrementSavedObjectsCreate.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail + const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); + registerCreateRoute(router, { coreUsageData }); await server.start(); }); @@ -55,7 +62,7 @@ describe('POST /api/saved_objects/{type}', () => { await server.stop(); }); - it('formats successful response', async () => { + it('formats successful response and records usage stats', async () => { const result = await supertest(httpSetup.server.listener) .post('/api/saved_objects/index-pattern') .send({ @@ -66,6 +73,9 @@ describe('POST /api/saved_objects/{type}', () => { .expect(200); expect(result.body).toEqual(clientResponse); + expect(coreUsageStatsClient.incrementSavedObjectsCreate).toHaveBeenCalledWith({ + request: expect.anything(), + }); }); it('requires attributes', async () => { diff --git a/src/core/server/saved_objects/routes/integration_tests/delete.test.ts b/src/core/server/saved_objects/routes/integration_tests/delete.test.ts index ff8642a34929f..c70754632980a 100644 --- a/src/core/server/saved_objects/routes/integration_tests/delete.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/delete.test.ts @@ -21,6 +21,9 @@ import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerDeleteRoute } from '../delete'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; +import { CoreUsageStatsClient } from '../../../core_usage_data'; +import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock'; +import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock'; import { setupServer } from '../test_utils'; type SetupServerReturn = UnwrapPromise>; @@ -30,13 +33,17 @@ describe('DELETE /api/saved_objects/{type}/{id}', () => { let httpSetup: SetupServerReturn['httpSetup']; let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; + let coreUsageStatsClient: jest.Mocked; beforeEach(async () => { ({ server, httpSetup, handlerContext } = await setupServer()); savedObjectsClient = handlerContext.savedObjects.client; const router = httpSetup.createRouter('/api/saved_objects/'); - registerDeleteRoute(router); + coreUsageStatsClient = coreUsageStatsClientMock.create(); + coreUsageStatsClient.incrementSavedObjectsDelete.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail + const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); + registerDeleteRoute(router, { coreUsageData }); await server.start(); }); @@ -45,12 +52,15 @@ describe('DELETE /api/saved_objects/{type}/{id}', () => { await server.stop(); }); - it('formats successful response', async () => { + it('formats successful response and records usage stats', async () => { const result = await supertest(httpSetup.server.listener) .delete('/api/saved_objects/index-pattern/logstash-*') .expect(200); expect(result.body).toEqual({}); + expect(coreUsageStatsClient.incrementSavedObjectsDelete).toHaveBeenCalledWith({ + request: expect.anything(), + }); }); it('calls upon savedObjectClient.delete', async () => { diff --git a/src/core/server/saved_objects/routes/integration_tests/export.test.ts b/src/core/server/saved_objects/routes/integration_tests/export.test.ts index c37ed2da97681..d5b1e492e573f 100644 --- a/src/core/server/saved_objects/routes/integration_tests/export.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/export.test.ts @@ -54,7 +54,7 @@ describe('POST /api/saved_objects/_export', () => { const router = httpSetup.createRouter('/api/saved_objects/'); coreUsageStatsClient = coreUsageStatsClientMock.create(); - coreUsageStatsClient.incrementSavedObjectsExport.mockRejectedValue(new Error('Oh no!')); // this error is intentionally swallowed so the export does not fail + coreUsageStatsClient.incrementSavedObjectsExport.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); registerExportRoute(router, { config, coreUsageData }); @@ -118,7 +118,7 @@ describe('POST /api/saved_objects/_export', () => { }) ); expect(coreUsageStatsClient.incrementSavedObjectsExport).toHaveBeenCalledWith({ - headers: expect.anything(), + request: expect.anything(), types: ['search'], supportedTypes: ['index-pattern', 'search'], }); diff --git a/src/core/server/saved_objects/routes/integration_tests/find.test.ts b/src/core/server/saved_objects/routes/integration_tests/find.test.ts index 9a426ef48c7da..8e3de04648b83 100644 --- a/src/core/server/saved_objects/routes/integration_tests/find.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/find.test.ts @@ -23,6 +23,9 @@ import querystring from 'querystring'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerFindRoute } from '../find'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; +import { CoreUsageStatsClient } from '../../../core_usage_data'; +import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock'; +import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock'; import { setupServer } from '../test_utils'; type SetupServerReturn = UnwrapPromise>; @@ -32,6 +35,7 @@ describe('GET /api/saved_objects/_find', () => { let httpSetup: SetupServerReturn['httpSetup']; let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; + let coreUsageStatsClient: jest.Mocked; const clientResponse = { total: 0, @@ -47,7 +51,10 @@ describe('GET /api/saved_objects/_find', () => { savedObjectsClient.find.mockResolvedValue(clientResponse); const router = httpSetup.createRouter('/api/saved_objects/'); - registerFindRoute(router); + coreUsageStatsClient = coreUsageStatsClientMock.create(); + coreUsageStatsClient.incrementSavedObjectsFind.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail + const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); + registerFindRoute(router, { coreUsageData }); await server.start(); }); @@ -66,7 +73,7 @@ describe('GET /api/saved_objects/_find', () => { ); }); - it('formats successful response', async () => { + it('formats successful response and records usage stats', async () => { const findResponse = { total: 2, per_page: 2, @@ -103,6 +110,9 @@ describe('GET /api/saved_objects/_find', () => { .expect(200); expect(result.body).toEqual(findResponse); + expect(coreUsageStatsClient.incrementSavedObjectsFind).toHaveBeenCalledWith({ + request: expect.anything(), + }); }); it('calls upon savedObjectClient.find with defaults', async () => { diff --git a/src/core/server/saved_objects/routes/integration_tests/get.test.ts b/src/core/server/saved_objects/routes/integration_tests/get.test.ts index 1e3405d7a318f..e05b6b09659fa 100644 --- a/src/core/server/saved_objects/routes/integration_tests/get.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/get.test.ts @@ -21,6 +21,9 @@ import supertest from 'supertest'; import { registerGetRoute } from '../get'; import { ContextService } from '../../../context'; import { savedObjectsClientMock } from '../../service/saved_objects_client.mock'; +import { CoreUsageStatsClient } from '../../../core_usage_data'; +import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock'; +import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock'; import { HttpService, InternalHttpServiceSetup } from '../../../http'; import { createHttpServer, createCoreContext } from '../../../http/test_utils'; import { coreMock } from '../../../mocks'; @@ -32,6 +35,7 @@ describe('GET /api/saved_objects/{type}/{id}', () => { let httpSetup: InternalHttpServiceSetup; let handlerContext: ReturnType; let savedObjectsClient: ReturnType; + let coreUsageStatsClient: jest.Mocked; beforeEach(async () => { const coreContext = createCoreContext({ coreId }); @@ -50,7 +54,10 @@ describe('GET /api/saved_objects/{type}/{id}', () => { }); const router = httpSetup.createRouter('/api/saved_objects/'); - registerGetRoute(router); + coreUsageStatsClient = coreUsageStatsClientMock.create(); + coreUsageStatsClient.incrementSavedObjectsGet.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail + const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); + registerGetRoute(router, { coreUsageData }); await server.start(); }); @@ -59,7 +66,7 @@ describe('GET /api/saved_objects/{type}/{id}', () => { await server.stop(); }); - it('formats successful response', async () => { + it('formats successful response and records usage stats', async () => { const clientResponse = { id: 'logstash-*', title: 'logstash-*', @@ -77,6 +84,9 @@ describe('GET /api/saved_objects/{type}/{id}', () => { .expect(200); expect(result.body).toEqual(clientResponse); + expect(coreUsageStatsClient.incrementSavedObjectsGet).toHaveBeenCalledWith({ + request: expect.anything(), + }); }); it('calls upon savedObjectClient.get', async () => { diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts index 9dfb7f79a925d..b80deb87725d4 100644 --- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts @@ -76,7 +76,7 @@ describe(`POST ${URL}`, () => { const router = httpSetup.createRouter('/internal/saved_objects/'); coreUsageStatsClient = coreUsageStatsClientMock.create(); - coreUsageStatsClient.incrementSavedObjectsImport.mockRejectedValue(new Error('Oh no!')); // this error is intentionally swallowed so the import does not fail + coreUsageStatsClient.incrementSavedObjectsImport.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); registerImportRoute(router, { config, coreUsageData }); @@ -106,7 +106,7 @@ describe(`POST ${URL}`, () => { expect(result.body).toEqual({ success: true, successCount: 0 }); expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // no objects were created expect(coreUsageStatsClient.incrementSavedObjectsImport).toHaveBeenCalledWith({ - headers: expect.anything(), + request: expect.anything(), createNewCopies: false, overwrite: false, }); diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts index 46f4d2435bf67..f135e34231cb6 100644 --- a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts @@ -82,7 +82,7 @@ describe(`POST ${URL}`, () => { const router = httpSetup.createRouter('/api/saved_objects/'); coreUsageStatsClient = coreUsageStatsClientMock.create(); coreUsageStatsClient.incrementSavedObjectsResolveImportErrors.mockRejectedValue( - new Error('Oh no!') // this error is intentionally swallowed so the export does not fail + new Error('Oh no!') // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail ); const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); registerResolveImportErrorsRoute(router, { config, coreUsageData }); @@ -117,7 +117,7 @@ describe(`POST ${URL}`, () => { expect(result.body).toEqual({ success: true, successCount: 0 }); expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // no objects were created expect(coreUsageStatsClient.incrementSavedObjectsResolveImportErrors).toHaveBeenCalledWith({ - headers: expect.anything(), + request: expect.anything(), createNewCopies: false, }); }); diff --git a/src/core/server/saved_objects/routes/integration_tests/update.test.ts b/src/core/server/saved_objects/routes/integration_tests/update.test.ts index dfccb651d72d7..433ffb49e05e4 100644 --- a/src/core/server/saved_objects/routes/integration_tests/update.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/update.test.ts @@ -21,6 +21,9 @@ import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { registerUpdateRoute } from '../update'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; +import { CoreUsageStatsClient } from '../../../core_usage_data'; +import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock'; +import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock'; import { setupServer } from '../test_utils'; type SetupServerReturn = UnwrapPromise>; @@ -30,6 +33,7 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => { let httpSetup: SetupServerReturn['httpSetup']; let handlerContext: SetupServerReturn['handlerContext']; let savedObjectsClient: ReturnType; + let coreUsageStatsClient: jest.Mocked; beforeEach(async () => { const clientResponse = { @@ -47,7 +51,10 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => { savedObjectsClient.update.mockResolvedValue(clientResponse); const router = httpSetup.createRouter('/api/saved_objects/'); - registerUpdateRoute(router); + coreUsageStatsClient = coreUsageStatsClientMock.create(); + coreUsageStatsClient.incrementSavedObjectsUpdate.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail + const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); + registerUpdateRoute(router, { coreUsageData }); await server.start(); }); @@ -56,7 +63,7 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => { await server.stop(); }); - it('formats successful response', async () => { + it('formats successful response and records usage stats', async () => { const clientResponse = { id: 'logstash-*', title: 'logstash-*', @@ -79,6 +86,9 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => { .expect(200); expect(result.body).toEqual(clientResponse); + expect(coreUsageStatsClient.incrementSavedObjectsUpdate).toHaveBeenCalledWith({ + request: expect.anything(), + }); }); it('calls upon savedObjectClient.update', async () => { diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index 34c178a975304..5db5454b224d7 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -83,10 +83,9 @@ export const registerResolveImportErrorsRoute = ( router.handleLegacyErrors(async (context, req, res) => { const { createNewCopies } = req.query; - const { headers } = req; const usageStatsClient = coreUsageData.getClient(); usageStatsClient - .incrementSavedObjectsResolveImportErrors({ headers, createNewCopies }) + .incrementSavedObjectsResolveImportErrors({ request: req, createNewCopies }) .catch(() => {}); const file = req.body.file as FileStream; diff --git a/src/core/server/saved_objects/routes/update.ts b/src/core/server/saved_objects/routes/update.ts index c0d94d362e648..95137cb6e77cf 100644 --- a/src/core/server/saved_objects/routes/update.ts +++ b/src/core/server/saved_objects/routes/update.ts @@ -19,8 +19,13 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '../../http'; +import { CoreUsageDataSetup } from '../../core_usage_data'; -export const registerUpdateRoute = (router: IRouter) => { +interface RouteDependencies { + coreUsageData: CoreUsageDataSetup; +} + +export const registerUpdateRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => { router.put( { path: '/{type}/{id}', @@ -49,6 +54,9 @@ export const registerUpdateRoute = (router: IRouter) => { const { attributes, version, references } = req.body; const options = { version, references }; + const usageStatsClient = coreUsageData.getClient(); + usageStatsClient.incrementSavedObjectsUpdate({ request: req }).catch(() => {}); + const result = await context.core.savedObjects.client.update(type, id, attributes, options); return res.ok({ body: result }); }) diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index a39bbecd16ff5..1ab06b7912d1f 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -541,24 +541,138 @@ export interface CoreUsageDataStart { // @internal export interface CoreUsageStats { + // (undocumented) + 'apiCalls.savedObjectsBulkCreate.namespace.custom.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsBulkCreate.namespace.custom.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsBulkCreate.namespace.custom.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsBulkCreate.namespace.default.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsBulkCreate.namespace.default.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsBulkCreate.namespace.default.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsBulkCreate.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsBulkGet.namespace.custom.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsBulkGet.namespace.custom.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsBulkGet.namespace.custom.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsBulkGet.namespace.default.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsBulkGet.namespace.default.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsBulkGet.namespace.default.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsBulkGet.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsBulkUpdate.namespace.custom.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsBulkUpdate.namespace.custom.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsBulkUpdate.namespace.custom.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsBulkUpdate.namespace.default.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsBulkUpdate.namespace.default.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsBulkUpdate.namespace.default.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsBulkUpdate.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsCreate.namespace.custom.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsCreate.namespace.custom.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsCreate.namespace.custom.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsCreate.namespace.default.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsCreate.namespace.default.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsCreate.namespace.default.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsCreate.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsDelete.namespace.custom.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsDelete.namespace.custom.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsDelete.namespace.custom.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsDelete.namespace.default.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsDelete.namespace.default.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsDelete.namespace.default.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsDelete.total'?: number; // (undocumented) 'apiCalls.savedObjectsExport.allTypesSelected.no'?: number; // (undocumented) 'apiCalls.savedObjectsExport.allTypesSelected.yes'?: number; // (undocumented) - 'apiCalls.savedObjectsExport.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsExport.namespace.custom.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsExport.namespace.custom.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsExport.namespace.custom.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsExport.namespace.default.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsExport.namespace.default.kibanaRequest.yes'?: number; // (undocumented) - 'apiCalls.savedObjectsExport.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsExport.namespace.default.total'?: number; // (undocumented) 'apiCalls.savedObjectsExport.total'?: number; // (undocumented) + 'apiCalls.savedObjectsFind.namespace.custom.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsFind.namespace.custom.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsFind.namespace.custom.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsFind.namespace.default.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsFind.namespace.default.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsFind.namespace.default.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsFind.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsGet.namespace.custom.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsGet.namespace.custom.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsGet.namespace.custom.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsGet.namespace.default.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsGet.namespace.default.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsGet.namespace.default.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsGet.total'?: number; + // (undocumented) 'apiCalls.savedObjectsImport.createNewCopiesEnabled.no'?: number; // (undocumented) 'apiCalls.savedObjectsImport.createNewCopiesEnabled.yes'?: number; // (undocumented) - 'apiCalls.savedObjectsImport.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsImport.namespace.custom.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsImport.namespace.custom.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsImport.namespace.custom.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsImport.namespace.default.kibanaRequest.no'?: number; // (undocumented) - 'apiCalls.savedObjectsImport.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsImport.namespace.default.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsImport.namespace.default.total'?: number; // (undocumented) 'apiCalls.savedObjectsImport.overwriteEnabled.no'?: number; // (undocumented) @@ -570,11 +684,33 @@ export interface CoreUsageStats { // (undocumented) 'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.yes'?: number; // (undocumented) - 'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsResolveImportErrors.namespace.custom.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsResolveImportErrors.namespace.custom.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsResolveImportErrors.namespace.custom.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsResolveImportErrors.namespace.default.kibanaRequest.no'?: number; // (undocumented) - 'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsResolveImportErrors.namespace.default.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsResolveImportErrors.namespace.default.total'?: number; // (undocumented) 'apiCalls.savedObjectsResolveImportErrors.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsUpdate.namespace.custom.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsUpdate.namespace.custom.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsUpdate.namespace.custom.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsUpdate.namespace.default.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsUpdate.namespace.default.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsUpdate.namespace.default.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsUpdate.total'?: number; } // @public (undocumented) diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 75530e557de04..08f0a191151dd 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -166,6 +166,7 @@ export class Server { const metricsSetup = await this.metrics.setup({ http: httpSetup }); const coreUsageDataSetup = this.coreUsageData.setup({ + http: httpSetup, metrics: metricsSetup, savedObjectsStartPromise: this.savedObjectsStartPromise, }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts index d30a3c5ab6861..a0960b30a2e87 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts @@ -115,21 +115,99 @@ export function getCoreUsageCollector( }, }, }, + // Saved Objects Client APIs + 'apiCalls.savedObjectsBulkCreate.total': { type: 'long' }, + 'apiCalls.savedObjectsBulkCreate.namespace.default.total': { type: 'long' }, + 'apiCalls.savedObjectsBulkCreate.namespace.default.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsBulkCreate.namespace.default.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsBulkCreate.namespace.custom.total': { type: 'long' }, + 'apiCalls.savedObjectsBulkCreate.namespace.custom.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsBulkCreate.namespace.custom.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsBulkGet.total': { type: 'long' }, + 'apiCalls.savedObjectsBulkGet.namespace.default.total': { type: 'long' }, + 'apiCalls.savedObjectsBulkGet.namespace.default.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsBulkGet.namespace.default.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsBulkGet.namespace.custom.total': { type: 'long' }, + 'apiCalls.savedObjectsBulkGet.namespace.custom.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsBulkGet.namespace.custom.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsBulkUpdate.total': { type: 'long' }, + 'apiCalls.savedObjectsBulkUpdate.namespace.default.total': { type: 'long' }, + 'apiCalls.savedObjectsBulkUpdate.namespace.default.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsBulkUpdate.namespace.default.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsBulkUpdate.namespace.custom.total': { type: 'long' }, + 'apiCalls.savedObjectsBulkUpdate.namespace.custom.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsBulkUpdate.namespace.custom.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsCreate.total': { type: 'long' }, + 'apiCalls.savedObjectsCreate.namespace.default.total': { type: 'long' }, + 'apiCalls.savedObjectsCreate.namespace.default.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsCreate.namespace.default.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsCreate.namespace.custom.total': { type: 'long' }, + 'apiCalls.savedObjectsCreate.namespace.custom.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsCreate.namespace.custom.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsDelete.total': { type: 'long' }, + 'apiCalls.savedObjectsDelete.namespace.default.total': { type: 'long' }, + 'apiCalls.savedObjectsDelete.namespace.default.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsDelete.namespace.default.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsDelete.namespace.custom.total': { type: 'long' }, + 'apiCalls.savedObjectsDelete.namespace.custom.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsDelete.namespace.custom.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsFind.total': { type: 'long' }, + 'apiCalls.savedObjectsFind.namespace.default.total': { type: 'long' }, + 'apiCalls.savedObjectsFind.namespace.default.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsFind.namespace.default.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsFind.namespace.custom.total': { type: 'long' }, + 'apiCalls.savedObjectsFind.namespace.custom.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsFind.namespace.custom.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsGet.total': { type: 'long' }, + 'apiCalls.savedObjectsGet.namespace.default.total': { type: 'long' }, + 'apiCalls.savedObjectsGet.namespace.default.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsGet.namespace.default.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsGet.namespace.custom.total': { type: 'long' }, + 'apiCalls.savedObjectsGet.namespace.custom.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsGet.namespace.custom.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsUpdate.total': { type: 'long' }, + 'apiCalls.savedObjectsUpdate.namespace.default.total': { type: 'long' }, + 'apiCalls.savedObjectsUpdate.namespace.default.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsUpdate.namespace.default.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsUpdate.namespace.custom.total': { type: 'long' }, + 'apiCalls.savedObjectsUpdate.namespace.custom.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsUpdate.namespace.custom.kibanaRequest.no': { type: 'long' }, + // Saved Objects Management APIs 'apiCalls.savedObjectsImport.total': { type: 'long' }, - 'apiCalls.savedObjectsImport.kibanaRequest.yes': { type: 'long' }, - 'apiCalls.savedObjectsImport.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsImport.namespace.default.total': { type: 'long' }, + 'apiCalls.savedObjectsImport.namespace.default.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsImport.namespace.default.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsImport.namespace.custom.total': { type: 'long' }, + 'apiCalls.savedObjectsImport.namespace.custom.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsImport.namespace.custom.kibanaRequest.no': { type: 'long' }, 'apiCalls.savedObjectsImport.createNewCopiesEnabled.yes': { type: 'long' }, 'apiCalls.savedObjectsImport.createNewCopiesEnabled.no': { type: 'long' }, 'apiCalls.savedObjectsImport.overwriteEnabled.yes': { type: 'long' }, 'apiCalls.savedObjectsImport.overwriteEnabled.no': { type: 'long' }, 'apiCalls.savedObjectsResolveImportErrors.total': { type: 'long' }, - 'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.yes': { type: 'long' }, - 'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsResolveImportErrors.namespace.default.total': { type: 'long' }, + 'apiCalls.savedObjectsResolveImportErrors.namespace.default.kibanaRequest.yes': { + type: 'long', + }, + 'apiCalls.savedObjectsResolveImportErrors.namespace.default.kibanaRequest.no': { + type: 'long', + }, + 'apiCalls.savedObjectsResolveImportErrors.namespace.custom.total': { type: 'long' }, + 'apiCalls.savedObjectsResolveImportErrors.namespace.custom.kibanaRequest.yes': { + type: 'long', + }, + 'apiCalls.savedObjectsResolveImportErrors.namespace.custom.kibanaRequest.no': { + type: 'long', + }, 'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.yes': { type: 'long' }, 'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.no': { type: 'long' }, 'apiCalls.savedObjectsExport.total': { type: 'long' }, - 'apiCalls.savedObjectsExport.kibanaRequest.yes': { type: 'long' }, - 'apiCalls.savedObjectsExport.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsExport.namespace.default.total': { type: 'long' }, + 'apiCalls.savedObjectsExport.namespace.default.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsExport.namespace.default.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsExport.namespace.custom.total': { type: 'long' }, + 'apiCalls.savedObjectsExport.namespace.custom.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsExport.namespace.custom.kibanaRequest.no': { type: 'long' }, 'apiCalls.savedObjectsExport.allTypesSelected.yes': { type: 'long' }, 'apiCalls.savedObjectsExport.allTypesSelected.no': { type: 'long' }, }, diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 55384329f9af7..d486c06568c1b 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -1517,13 +1517,193 @@ } } }, + "apiCalls.savedObjectsBulkCreate.total": { + "type": "long" + }, + "apiCalls.savedObjectsBulkCreate.namespace.default.total": { + "type": "long" + }, + "apiCalls.savedObjectsBulkCreate.namespace.default.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsBulkCreate.namespace.default.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsBulkCreate.namespace.custom.total": { + "type": "long" + }, + "apiCalls.savedObjectsBulkCreate.namespace.custom.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsBulkCreate.namespace.custom.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsBulkGet.total": { + "type": "long" + }, + "apiCalls.savedObjectsBulkGet.namespace.default.total": { + "type": "long" + }, + "apiCalls.savedObjectsBulkGet.namespace.default.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsBulkGet.namespace.default.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsBulkGet.namespace.custom.total": { + "type": "long" + }, + "apiCalls.savedObjectsBulkGet.namespace.custom.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsBulkGet.namespace.custom.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsBulkUpdate.total": { + "type": "long" + }, + "apiCalls.savedObjectsBulkUpdate.namespace.default.total": { + "type": "long" + }, + "apiCalls.savedObjectsBulkUpdate.namespace.default.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsBulkUpdate.namespace.default.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsBulkUpdate.namespace.custom.total": { + "type": "long" + }, + "apiCalls.savedObjectsBulkUpdate.namespace.custom.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsBulkUpdate.namespace.custom.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsCreate.total": { + "type": "long" + }, + "apiCalls.savedObjectsCreate.namespace.default.total": { + "type": "long" + }, + "apiCalls.savedObjectsCreate.namespace.default.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsCreate.namespace.default.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsCreate.namespace.custom.total": { + "type": "long" + }, + "apiCalls.savedObjectsCreate.namespace.custom.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsCreate.namespace.custom.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsDelete.total": { + "type": "long" + }, + "apiCalls.savedObjectsDelete.namespace.default.total": { + "type": "long" + }, + "apiCalls.savedObjectsDelete.namespace.default.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsDelete.namespace.default.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsDelete.namespace.custom.total": { + "type": "long" + }, + "apiCalls.savedObjectsDelete.namespace.custom.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsDelete.namespace.custom.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsFind.total": { + "type": "long" + }, + "apiCalls.savedObjectsFind.namespace.default.total": { + "type": "long" + }, + "apiCalls.savedObjectsFind.namespace.default.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsFind.namespace.default.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsFind.namespace.custom.total": { + "type": "long" + }, + "apiCalls.savedObjectsFind.namespace.custom.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsFind.namespace.custom.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsGet.total": { + "type": "long" + }, + "apiCalls.savedObjectsGet.namespace.default.total": { + "type": "long" + }, + "apiCalls.savedObjectsGet.namespace.default.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsGet.namespace.default.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsGet.namespace.custom.total": { + "type": "long" + }, + "apiCalls.savedObjectsGet.namespace.custom.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsGet.namespace.custom.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsUpdate.total": { + "type": "long" + }, + "apiCalls.savedObjectsUpdate.namespace.default.total": { + "type": "long" + }, + "apiCalls.savedObjectsUpdate.namespace.default.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsUpdate.namespace.default.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsUpdate.namespace.custom.total": { + "type": "long" + }, + "apiCalls.savedObjectsUpdate.namespace.custom.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsUpdate.namespace.custom.kibanaRequest.no": { + "type": "long" + }, "apiCalls.savedObjectsImport.total": { "type": "long" }, - "apiCalls.savedObjectsImport.kibanaRequest.yes": { + "apiCalls.savedObjectsImport.namespace.default.total": { + "type": "long" + }, + "apiCalls.savedObjectsImport.namespace.default.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsImport.namespace.default.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsImport.namespace.custom.total": { + "type": "long" + }, + "apiCalls.savedObjectsImport.namespace.custom.kibanaRequest.yes": { "type": "long" }, - "apiCalls.savedObjectsImport.kibanaRequest.no": { + "apiCalls.savedObjectsImport.namespace.custom.kibanaRequest.no": { "type": "long" }, "apiCalls.savedObjectsImport.createNewCopiesEnabled.yes": { @@ -1541,10 +1721,22 @@ "apiCalls.savedObjectsResolveImportErrors.total": { "type": "long" }, - "apiCalls.savedObjectsResolveImportErrors.kibanaRequest.yes": { + "apiCalls.savedObjectsResolveImportErrors.namespace.default.total": { + "type": "long" + }, + "apiCalls.savedObjectsResolveImportErrors.namespace.default.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsResolveImportErrors.namespace.default.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsResolveImportErrors.namespace.custom.total": { + "type": "long" + }, + "apiCalls.savedObjectsResolveImportErrors.namespace.custom.kibanaRequest.yes": { "type": "long" }, - "apiCalls.savedObjectsResolveImportErrors.kibanaRequest.no": { + "apiCalls.savedObjectsResolveImportErrors.namespace.custom.kibanaRequest.no": { "type": "long" }, "apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.yes": { @@ -1556,10 +1748,22 @@ "apiCalls.savedObjectsExport.total": { "type": "long" }, - "apiCalls.savedObjectsExport.kibanaRequest.yes": { + "apiCalls.savedObjectsExport.namespace.default.total": { + "type": "long" + }, + "apiCalls.savedObjectsExport.namespace.default.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsExport.namespace.default.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsExport.namespace.custom.total": { + "type": "long" + }, + "apiCalls.savedObjectsExport.namespace.custom.kibanaRequest.yes": { "type": "long" }, - "apiCalls.savedObjectsExport.kibanaRequest.no": { + "apiCalls.savedObjectsExport.namespace.custom.kibanaRequest.no": { "type": "long" }, "apiCalls.savedObjectsExport.allTypesSelected.yes": { diff --git a/test/api_integration/apis/saved_objects/bulk_create.js b/test/api_integration/apis/saved_objects/bulk_create.js index 7db968df8357a..a78acea1d0299 100644 --- a/test/api_integration/apis/saved_objects/bulk_create.js +++ b/test/api_integration/apis/saved_objects/bulk_create.js @@ -68,7 +68,7 @@ export default function ({ getService }) { type: 'dashboard', id: 'a01b2f57-fcfd-4864-b735-09e28f0d815e', updated_at: resp.body.saved_objects[1].updated_at, - version: 'WzgsMV0=', + version: resp.body.saved_objects[1].version, attributes: { title: 'A great new dashboard', }, @@ -117,7 +117,7 @@ export default function ({ getService }) { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', updated_at: resp.body.saved_objects[0].updated_at, - version: 'WzAsMV0=', + version: resp.body.saved_objects[0].version, attributes: { title: 'An existing visualization', }, @@ -131,7 +131,7 @@ export default function ({ getService }) { type: 'dashboard', id: 'a01b2f57-fcfd-4864-b735-09e28f0d815e', updated_at: resp.body.saved_objects[1].updated_at, - version: 'WzEsMV0=', + version: resp.body.saved_objects[1].version, attributes: { title: 'A great new dashboard', }, diff --git a/test/api_integration/apis/saved_objects/bulk_update.js b/test/api_integration/apis/saved_objects/bulk_update.js index 973ce382ea813..58c72575c04bb 100644 --- a/test/api_integration/apis/saved_objects/bulk_update.js +++ b/test/api_integration/apis/saved_objects/bulk_update.js @@ -61,7 +61,7 @@ export default function ({ getService }) { expect(_.omit(firstObject, ['updated_at'])).to.eql({ id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', type: 'visualization', - version: 'WzgsMV0=', + version: firstObject.version, attributes: { title: 'An existing visualization', }, @@ -74,7 +74,7 @@ export default function ({ getService }) { expect(_.omit(secondObject, ['updated_at'])).to.eql({ id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', type: 'dashboard', - version: 'WzksMV0=', + version: secondObject.version, attributes: { title: 'An existing dashboard', }, diff --git a/test/api_integration/apis/saved_objects/create.js b/test/api_integration/apis/saved_objects/create.js index c1300125441bc..15aecb6e547a0 100644 --- a/test/api_integration/apis/saved_objects/create.js +++ b/test/api_integration/apis/saved_objects/create.js @@ -53,7 +53,7 @@ export default function ({ getService }) { type: 'visualization', migrationVersion: resp.body.migrationVersion, updated_at: resp.body.updated_at, - version: 'WzgsMV0=', + version: resp.body.version, attributes: { title: 'My favorite vis', }, @@ -100,7 +100,7 @@ export default function ({ getService }) { type: 'visualization', migrationVersion: resp.body.migrationVersion, updated_at: resp.body.updated_at, - version: 'WzAsMV0=', + version: resp.body.version, attributes: { title: 'My favorite vis', }, diff --git a/test/api_integration/apis/saved_objects/update.js b/test/api_integration/apis/saved_objects/update.js index 7803c39897f28..14b363a512ea1 100644 --- a/test/api_integration/apis/saved_objects/update.js +++ b/test/api_integration/apis/saved_objects/update.js @@ -52,7 +52,7 @@ export default function ({ getService }) { id: resp.body.id, type: 'visualization', updated_at: resp.body.updated_at, - version: 'WzgsMV0=', + version: resp.body.version, attributes: { title: 'My second favorite vis', }, diff --git a/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.test.ts b/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.test.ts index b313c0be32b95..20d38d5809149 100644 --- a/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.test.ts +++ b/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.test.ts @@ -22,7 +22,7 @@ describe('UsageStatsClient', () => { return { usageStatsClient, debugLoggerMock, repositoryMock }; }; - const firstPartyRequestHeaders = { 'kbn-version': 'a', origin: 'b', referer: 'c' }; // as long as these three header fields are truthy, this will be treated like a first-party request + const firstPartyRequestHeaders = { 'kbn-version': 'a', referer: 'b' }; // as long as these two header fields are truthy, this will be treated like a first-party request const incrementOptions = { refresh: false }; describe('#getUsageStats', () => { diff --git a/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.ts b/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.ts index 4c9d11a11ccca..22e8bb5c4d39a 100644 --- a/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.ts +++ b/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.ts @@ -102,7 +102,7 @@ export class UsageStatsClient { } function getIsKibanaRequest(headers?: Headers) { - // The presence of these three request headers gives us a good indication that this is a first-party request from the Kibana client. + // The presence of these two request headers gives us a good indication that this is a first-party request from the Kibana client. // We can't be 100% certain, but this is a reasonable attempt. - return headers && headers['kbn-version'] && headers.origin && headers.referer; + return headers && headers['kbn-version'] && headers.referer; } From 84d7b9e4ac01244a0b772d73abcde551d913552c Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Mon, 14 Dec 2020 15:18:29 -0700 Subject: [PATCH 28/44] Upgrade EUI to v30.6.0 and update jest snapshots (#85504) --- package.json | 2 +- .../collapsible_nav.test.tsx.snap | 2 ++ .../header/__snapshots__/header.test.tsx.snap | 1 + .../dashboard_empty_screen.test.tsx.snap | 5 +++ .../saved_objects_installer.test.js.snap | 12 +++++++ .../header/__snapshots__/header.test.tsx.snap | 3 ++ .../warning_call_out.test.tsx.snap | 2 ++ .../inspector_panel.test.tsx.snap | 1 + .../__snapshots__/header.test.tsx.snap | 4 +++ .../__test__/__snapshots__/List.test.tsx.snap | 2 ++ .../time_filter.stories.storyshot | 5 +++ .../__snapshots__/asset.stories.storyshot | 8 +++++ .../asset_manager.stories.storyshot | 12 +++++++ .../color_palette.stories.storyshot | 36 +++++++++++++++++++ .../color_picker.stories.storyshot | 21 +++++++++++ .../color_picker_popover.stories.storyshot | 4 +++ .../custom_element_modal.stories.storyshot | 12 +++++++ .../datasource_component.stories.storyshot | 3 ++ .../element_card.stories.storyshot | 2 ++ .../keyboard_shortcuts_doc.stories.storyshot | 1 + .../element_controls.stories.storyshot | 2 ++ .../element_grid.stories.storyshot | 9 +++++ .../saved_elements_modal.stories.storyshot | 18 ++++++++++ .../shape_picker.stories.storyshot | 2 ++ .../shape_picker_popover.stories.storyshot | 3 ++ .../sidebar_header.stories.storyshot | 4 +++ .../text_style_picker.stories.storyshot | 8 +++++ .../__snapshots__/toolbar.stories.storyshot | 5 +++ .../delete_var.stories.storyshot | 2 ++ .../__snapshots__/edit_var.stories.storyshot | 4 +++ .../var_config.stories.storyshot | 1 + .../__snapshots__/edit_menu.stories.storyshot | 6 ++++ .../element_menu.stories.storyshot | 1 + .../__snapshots__/pdf_panel.stories.storyshot | 1 + .../share_menu.stories.storyshot | 1 + .../__snapshots__/view_menu.stories.storyshot | 4 +++ .../workpad_templates.stories.storyshot | 4 +++ .../extended_template.stories.storyshot | 3 ++ .../simple_template.stories.storyshot | 2 ++ .../simple_template.stories.storyshot | 5 +++ .../__snapshots__/canvas.stories.storyshot | 6 ++++ .../__snapshots__/footer.stories.storyshot | 4 +++ .../page_controls.stories.storyshot | 3 ++ .../__snapshots__/settings.stories.storyshot | 2 ++ .../extend_index_management.test.tsx.snap | 2 ++ .../upload_license.test.tsx.snap | 20 +++++++++++ .../upgrade_failure.test.js.snap | 12 +++++++ .../collection_enabled.test.js.snap | 3 ++ .../collection_interval.test.js.snap | 3 ++ .../remote_cluster_form.test.js.snap | 3 ++ .../report_info_button.test.tsx.snap | 5 +++ .../overwritten_session_page.test.tsx.snap | 1 + .../__snapshots__/link_to_app.test.tsx.snap | 1 + .../__snapshots__/index.test.tsx.snap | 2 ++ .../__snapshots__/empty_state.test.tsx.snap | 6 ++++ yarn.lock | 11 +++--- 56 files changed, 301 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index ba6ac1e70248b..9ee9df67b8aea 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "@elastic/datemath": "link:packages/elastic-datemath", "@elastic/elasticsearch": "7.10.0", "@elastic/ems-client": "7.11.0", - "@elastic/eui": "30.5.1", + "@elastic/eui": "30.6.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "^9.0.1-kibana3", "@elastic/node-crypto": "1.2.1", diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 201f2e5f8f14b..c836686ec602b 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -1974,6 +1974,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` >