diff --git a/packages/kbn-es-query/src/filters/build_filters/phrase_filter.test.ts b/packages/kbn-es-query/src/filters/build_filters/phrase_filter.test.ts index 1088ba196840c..f2fc685b2e180 100644 --- a/packages/kbn-es-query/src/filters/build_filters/phrase_filter.test.ts +++ b/packages/kbn-es-query/src/filters/build_filters/phrase_filter.test.ts @@ -134,7 +134,7 @@ describe('buildInlineScriptForPhraseFilter', () => { }; const expected = - `boolean compare(Supplier s, def v) {return s.get() == v;}` + + `boolean compare(Supplier s, def v) {if(s.get() instanceof List){List list = s.get(); for(def k : list){if(k==v){return true;}}return false;}else{return s.get() == v;}}` + `compare(() -> { return foo; }, params.value);`; expect(buildInlineScriptForPhraseFilter(field)).toBe(expected); diff --git a/packages/kbn-es-query/src/filters/build_filters/phrase_filter.ts b/packages/kbn-es-query/src/filters/build_filters/phrase_filter.ts index 3bbf94cd0722a..4adc8fdc0fdd4 100644 --- a/packages/kbn-es-query/src/filters/build_filters/phrase_filter.ts +++ b/packages/kbn-es-query/src/filters/build_filters/phrase_filter.ts @@ -148,7 +148,7 @@ export const buildInlineScriptForPhraseFilter = (scriptedField: DataViewFieldBas // We must wrap painless scripts in a lambda in case they're more than a simple expression if (scriptedField.lang === 'painless') { return ( - `boolean compare(Supplier s, def v) {return s.get() == v;}` + + `boolean compare(Supplier s, def v) {if(s.get() instanceof List){List list = s.get(); for(def k : list){if(k==v){return true;}}return false;}else{return s.get() == v;}}` + `compare(() -> { ${scriptedField.script} }, params.value);` ); } else { diff --git a/src/plugins/dev_tools/public/application.tsx b/src/plugins/dev_tools/public/application.tsx index 1a1d7ca9fb430..0499c4ab5ccb1 100644 --- a/src/plugins/dev_tools/public/application.tsx +++ b/src/plugins/dev_tools/public/application.tsx @@ -108,9 +108,6 @@ function DevToolsWrapper({ label={i18n.translate('devTools.badge.betaLabel', { defaultMessage: 'Beta', })} - tooltipContent={i18n.translate('devTools.badge.betaTooltipText', { - defaultMessage: 'This feature might change drastically in future releases', - })} /> )} diff --git a/src/plugins/guided_onboarding/public/components/guide_panel.styles.ts b/src/plugins/guided_onboarding/public/components/guide_panel.styles.ts index 46ad2de0906fc..4768d4112bcea 100644 --- a/src/plugins/guided_onboarding/public/components/guide_panel.styles.ts +++ b/src/plugins/guided_onboarding/public/components/guide_panel.styles.ts @@ -36,7 +36,7 @@ export const getGuidePanelStyles = (euiTheme: EuiThemeComputed) => ({ } `, flyoutBody: css` - overflow: scroll; + overflow: auto; .euiFlyoutBody__overflowContent { width: 480px; padding-top: 10px; diff --git a/x-pack/plugins/apm/public/components/app/mobile/charts/http_requests_chart.tsx b/x-pack/plugins/apm/public/components/app/mobile/charts/http_requests_chart.tsx index 0b530a6cc4e2c..e625c94a9d167 100644 --- a/x-pack/plugins/apm/public/components/app/mobile/charts/http_requests_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/mobile/charts/http_requests_chart.tsx @@ -23,8 +23,8 @@ import { import { usePreviousPeriodLabel } from '../../../../hooks/use_previous_period_text'; const INITIAL_STATE = { - currentPeriod: [], - previousPeriod: [], + currentPeriod: { timeseries: [] }, + previousPeriod: { timeseries: [] }, }; export function HttpRequestsChart({ @@ -89,7 +89,7 @@ export function HttpRequestsChart({ const timeseries = [ { - data: data.currentPeriod, + data: data.currentPeriod.timeseries, type: 'linemark', color: currentPeriodColor, title: i18n.translate('xpack.apm.transactions.httpRequestsTitle', { @@ -99,7 +99,7 @@ export function HttpRequestsChart({ ...(comparisonEnabled ? [ { - data: data.previousPeriod, + data: data.previousPeriod.timeseries, type: 'area', color: previousPeriodColor, title: previousPeriodLabel, diff --git a/x-pack/plugins/apm/public/components/app/mobile/charts/sessions_chart.tsx b/x-pack/plugins/apm/public/components/app/mobile/charts/sessions_chart.tsx index 8c0b806cce6be..cfbc9864e4612 100644 --- a/x-pack/plugins/apm/public/components/app/mobile/charts/sessions_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/mobile/charts/sessions_chart.tsx @@ -24,8 +24,8 @@ import { import { usePreviousPeriodLabel } from '../../../../hooks/use_previous_period_text'; const INITIAL_STATE = { - currentPeriod: [], - previousPeriod: [], + currentPeriod: { timeseries: [] }, + previousPeriod: { timeseries: [] }, }; type SessionsChart = @@ -95,7 +95,7 @@ export function SessionsChart({ const timeseries = [ { - data: data.currentPeriod, + data: data.currentPeriod.timeseries, type: 'linemark', color: currentPeriodColor, title: i18n.translate('xpack.apm.transactions.sessionsChartTitle', { @@ -105,7 +105,7 @@ export function SessionsChart({ ...(comparisonEnabled ? [ { - data: data.previousPeriod, + data: data.previousPeriod.timeseries, type: 'area', color: previousPeriodColor, title: previousPeriodLabel, diff --git a/x-pack/plugins/apm/public/components/app/mobile/service_overview/stats/index.tsx b/x-pack/plugins/apm/public/components/app/mobile/service_overview/stats/index.tsx index 744cfb13f3c75..d94e988ba6d71 100644 --- a/x-pack/plugins/apm/public/components/app/mobile/service_overview/stats/index.tsx +++ b/x-pack/plugins/apm/public/components/app/mobile/service_overview/stats/index.tsx @@ -74,9 +74,9 @@ export function MobileStats({ defaultMessage: 'Crash Rate', }), icon: getIcon('bug'), - value: data?.crashCount?.value ?? NaN, + value: 'N/A', valueFormatter: (value: number) => valueFormatter(value, 'cpm'), - trend: data?.maxLoadTime?.timeseries, + trend: [], trendShape: MetricTrendShape.Area, }, { @@ -85,9 +85,9 @@ export function MobileStats({ defaultMessage: 'Slowest App load time', }), icon: getIcon('visGauge'), - value: data?.maxLoadTime?.value ?? NaN, + value: 'N/A', valueFormatter: (value: number) => valueFormatter(value, 's'), - trend: data?.maxLoadTime.timeseries, + trend: [], trendShape: MetricTrendShape.Area, }, { @@ -96,9 +96,9 @@ export function MobileStats({ defaultMessage: 'Sessions', }), icon: getIcon('timeslider'), - value: data?.sessions?.value ?? NaN, + value: data?.currentPeriod?.sessions?.value ?? NaN, valueFormatter: (value: number) => valueFormatter(value), - trend: data?.sessions.timeseries, + trend: data?.currentPeriod?.sessions?.timeseries, trendShape: MetricTrendShape.Area, }, { @@ -107,9 +107,9 @@ export function MobileStats({ defaultMessage: 'HTTP requests', }), icon: getIcon('kubernetesPod'), - value: data?.requests?.value ?? NaN, + value: data?.currentPeriod?.requests?.value ?? NaN, valueFormatter: (value: number) => valueFormatter(value), - trend: data?.requests.timeseries, + trend: data?.currentPeriod?.requests?.timeseries ?? [], trendShape: MetricTrendShape.Area, }, ]; diff --git a/x-pack/plugins/apm/server/routes/mobile/get_mobile_filters.ts b/x-pack/plugins/apm/server/routes/mobile/get_mobile_filters.ts index cc648513a896a..ff6292eddb27a 100644 --- a/x-pack/plugins/apm/server/routes/mobile/get_mobile_filters.ts +++ b/x-pack/plugins/apm/server/routes/mobile/get_mobile_filters.ts @@ -52,12 +52,7 @@ export async function getMobileFilters({ }): Promise { const response = await apmEventClient.search('get_mobile_filters', { apm: { - events: [ - ProcessorEvent.error, - ProcessorEvent.metric, - ProcessorEvent.transaction, - ProcessorEvent.span, - ], + events: [ProcessorEvent.transaction], }, body: { track_total_hits: false, diff --git a/x-pack/plugins/apm/server/routes/mobile/get_http_requests_chart.ts b/x-pack/plugins/apm/server/routes/mobile/get_mobile_http_requests.ts similarity index 78% rename from x-pack/plugins/apm/server/routes/mobile/get_http_requests_chart.ts rename to x-pack/plugins/apm/server/routes/mobile/get_mobile_http_requests.ts index e1e9ef59c94e8..d2c7abe691d7a 100644 --- a/x-pack/plugins/apm/server/routes/mobile/get_http_requests_chart.ts +++ b/x-pack/plugins/apm/server/routes/mobile/get_mobile_http_requests.ts @@ -15,6 +15,7 @@ import { SERVICE_NAME, TRANSACTION_NAME, SERVICE_TARGET_TYPE, + METRICSET_NAME, } from '../../../common/es_fields/apm'; import { environmentQuery } from '../../../common/utils/environment_query'; import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms'; @@ -22,10 +23,11 @@ import { offsetPreviousPeriodCoordinates } from '../../../common/utils/offset_pr import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; import { getBucketSize } from '../../lib/helpers/get_bucket_size'; import { Coordinate } from '../../../typings/timeseries'; +import { Maybe } from '../../../typings/common'; export interface HttpRequestsTimeseries { - currentPeriod: Coordinate[]; - previousPeriod: Coordinate[]; + currentPeriod: { timeseries: Coordinate[]; value: Maybe }; + previousPeriod: { timeseries: Coordinate[]; value: Maybe }; } interface Props { apmEventClient: APMEventClient; @@ -60,6 +62,12 @@ async function getHttpRequestsTimeseries({ minBucketSize: 60, }); + const aggs = { + requests: { + filter: { term: { [SERVICE_TARGET_TYPE]: 'http' } }, + }, + }; + const response = await apmEventClient.search('get_http_requests_chart', { apm: { events: [ProcessorEvent.metric] }, body: { @@ -69,6 +77,7 @@ async function getHttpRequestsTimeseries({ bool: { filter: [ { exists: { field: SERVICE_TARGET_TYPE } }, + ...termQuery(METRICSET_NAME, 'service_destination'), ...termQuery(SERVICE_NAME, serviceName), ...termQuery(TRANSACTION_NAME, transactionName), ...rangeQuery(startWithOffset, endWithOffset), @@ -85,27 +94,28 @@ async function getHttpRequestsTimeseries({ min_doc_count: 0, extended_bounds: { min: startWithOffset, max: endWithOffset }, }, - aggs: { - requests: { - filter: { term: { [SERVICE_TARGET_TYPE]: 'http' } }, - }, - }, + aggs, }, + ...aggs, }, }, }); - return ( + const timeseries = response?.aggregations?.timeseries.buckets.map((bucket) => { return { x: bucket.key, - y: bucket.doc_count ?? 0, + y: bucket.requests.doc_count, }; - }) ?? [] - ); + }) ?? []; + + return { + timeseries, + value: response.aggregations?.requests?.doc_count, + }; } -export async function getHttpRequestsChart({ +export async function getMobileHttpRequests({ kuery, apmEventClient, serviceName, @@ -136,7 +146,7 @@ export async function getHttpRequestsChart({ end, offset, }) - : []; + : { timeseries: [], value: null }; const [currentPeriod, previousPeriod] = await Promise.all([ currentPeriodPromise, @@ -145,9 +155,12 @@ export async function getHttpRequestsChart({ return { currentPeriod, - previousPeriod: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: currentPeriod, - previousPeriodTimeseries: previousPeriod, - }), + previousPeriod: { + timeseries: offsetPreviousPeriodCoordinates({ + currentPeriodTimeseries: currentPeriod.timeseries, + previousPeriodTimeseries: previousPeriod.timeseries, + }), + value: previousPeriod?.value, + }, }; } diff --git a/x-pack/plugins/apm/server/routes/mobile/get_sessions_chart.ts b/x-pack/plugins/apm/server/routes/mobile/get_mobile_sessions.ts similarity index 77% rename from x-pack/plugins/apm/server/routes/mobile/get_sessions_chart.ts rename to x-pack/plugins/apm/server/routes/mobile/get_mobile_sessions.ts index 874743af63342..3414a91b00c2f 100644 --- a/x-pack/plugins/apm/server/routes/mobile/get_sessions_chart.ts +++ b/x-pack/plugins/apm/server/routes/mobile/get_mobile_sessions.ts @@ -22,10 +22,11 @@ import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms'; import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; import { getBucketSize } from '../../lib/helpers/get_bucket_size'; import { Coordinate } from '../../../typings/timeseries'; +import { Maybe } from '../../../typings/common'; export interface SessionsTimeseries { - currentPeriod: Coordinate[]; - previousPeriod: Coordinate[]; + currentPeriod: { timeseries: Coordinate[]; value: Maybe }; + previousPeriod: { timeseries: Coordinate[]; value: Maybe }; } interface Props { @@ -61,13 +62,15 @@ async function getSessionTimeseries({ minBucketSize: 60, }); - const response = await apmEventClient.search('get_sessions_chart', { + const aggs = { + sessions: { + cardinality: { field: SESSION_ID }, + }, + }; + + const response = await apmEventClient.search('get_mobile_sessions', { apm: { - events: [ - ProcessorEvent.transaction, - ProcessorEvent.error, - ProcessorEvent.span, - ], + events: [ProcessorEvent.transaction], }, body: { track_total_hits: false, @@ -92,27 +95,28 @@ async function getSessionTimeseries({ min_doc_count: 0, extended_bounds: { min: startWithOffset, max: endWithOffset }, }, - aggs: { - sessions: { - cardinality: { field: SESSION_ID }, - }, - }, + aggs, }, + ...aggs, }, }, }); - return ( + const timeseries = response?.aggregations?.timeseries.buckets.map((bucket) => { return { x: bucket.key, - y: bucket.doc_count ?? 0, + y: bucket.sessions.value, }; - }) ?? [] - ); + }) ?? []; + + return { + timeseries, + value: response.aggregations?.sessions?.value, + }; } -export async function getSessionsChart({ +export async function getMobileSessions({ kuery, apmEventClient, serviceName, @@ -143,7 +147,7 @@ export async function getSessionsChart({ end, offset, }) - : []; + : { timeseries: [], value: null }; const [currentPeriod, previousPeriod] = await Promise.all([ currentPeriodPromise, @@ -152,9 +156,12 @@ export async function getSessionsChart({ return { currentPeriod, - previousPeriod: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: currentPeriod, - previousPeriodTimeseries: previousPeriod, - }), + previousPeriod: { + timeseries: offsetPreviousPeriodCoordinates({ + currentPeriodTimeseries: currentPeriod.timeseries, + previousPeriodTimeseries: previousPeriod.timeseries, + }), + value: previousPeriod?.value, + }, }; } diff --git a/x-pack/plugins/apm/server/routes/mobile/get_mobile_stats.ts b/x-pack/plugins/apm/server/routes/mobile/get_mobile_stats.ts index e37f4ec9616c8..70d51360ff89d 100644 --- a/x-pack/plugins/apm/server/routes/mobile/get_mobile_stats.ts +++ b/x-pack/plugins/apm/server/routes/mobile/get_mobile_stats.ts @@ -5,140 +5,117 @@ * 2.0. */ -import { - termQuery, - kqlQuery, - rangeQuery, -} from '@kbn/observability-plugin/server'; -import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { - SERVICE_NAME, - SESSION_ID, - SERVICE_TARGET_TYPE, - APP_LAUNCH_TIME, - EVENT_NAME, -} from '../../../common/es_fields/apm'; -import { environmentQuery } from '../../../common/utils/environment_query'; import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; -import { getBucketSize } from '../../lib/helpers/get_bucket_size'; +import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms'; +import { getMobileSessions } from './get_mobile_sessions'; +import { getMobileHttpRequests } from './get_mobile_http_requests'; +import { Maybe } from '../../../typings/common'; -type Timeseries = Array<{ x: number; y: number }>; -export interface MobileStats { - sessions: { value?: number; timeseries: Timeseries }; - requests: { value?: number | null; timeseries: Timeseries }; - maxLoadTime: { value?: number | null; timeseries: Timeseries }; - crashCount: { value?: number | null; timeseries: Timeseries }; +export interface Timeseries { + x: number; + y: number; +} +interface MobileStats { + sessions: { timeseries: Timeseries[]; value: Maybe }; + requests: { timeseries: Timeseries[]; value: Maybe }; } -export async function getMobileStats({ - kuery, - apmEventClient, - serviceName, - transactionType, - environment, - start, - end, -}: { +export interface MobilePeriodStats { + currentPeriod: MobileStats; + previousPeriod: MobileStats; +} + +interface Props { kuery: string; apmEventClient: APMEventClient; serviceName: string; - transactionType?: string; environment: string; start: number; end: number; -}): Promise { - const { intervalString } = getBucketSize({ + offset?: string; +} + +async function getMobileStats({ + kuery, + apmEventClient, + serviceName, + environment, + start, + end, + offset, +}: Props): Promise { + const { startWithOffset, endWithOffset } = getOffsetInMs({ start, end, - minBucketSize: 60, + offset, }); - const aggs = { + const commonProps = { + kuery, + apmEventClient, + serviceName, + environment, + start: startWithOffset, + end: endWithOffset, + offset, + }; + + const [sessions, httpRequests] = await Promise.all([ + getMobileSessions({ ...commonProps }), + getMobileHttpRequests({ ...commonProps }), + ]); + + return { sessions: { - cardinality: { field: SESSION_ID }, + value: sessions.currentPeriod.value, + timeseries: sessions.currentPeriod.timeseries as Timeseries[], }, requests: { - filter: { term: { [SERVICE_TARGET_TYPE]: 'http' } }, - }, - maxLoadTime: { - max: { field: APP_LAUNCH_TIME }, - }, - crashCount: { - filter: { term: { [EVENT_NAME]: 'crash' } }, + value: httpRequests.currentPeriod.value, + timeseries: httpRequests.currentPeriod.timeseries as Timeseries[], }, }; +} - const response = await apmEventClient.search('get_mobile_stats', { - apm: { - events: [ - ProcessorEvent.error, - ProcessorEvent.metric, - ProcessorEvent.transaction, - ProcessorEvent.span, - ], - }, - body: { - track_total_hits: false, - size: 0, - query: { - bool: { - filter: [ - ...termQuery(SERVICE_NAME, serviceName), - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ], - }, - }, - aggs: { - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: intervalString, - min_doc_count: 0, - }, - aggs, - }, - ...aggs, - }, - }, +export async function getMobileStatsPeriods({ + kuery, + apmEventClient, + serviceName, + environment, + start, + end, + offset, +}: Props): Promise { + const commonProps = { + kuery, + apmEventClient, + serviceName, + environment, + start, + end, + }; + + const currentPeriodPromise = getMobileStats({ + ...commonProps, }); - const durationAsMinutes = (end - start) / 1000 / 60; + const previousPeriodPromise = offset + ? getMobileStats({ + ...commonProps, + offset, + }) + : { + sessions: { timeseries: [], value: null }, + requests: { timeseries: [], value: null }, + }; + + const [currentPeriod, previousPeriod] = await Promise.all([ + currentPeriodPromise, + previousPeriodPromise, + ]); return { - sessions: { - value: response.aggregations?.sessions?.value, - timeseries: - response.aggregations?.timeseries?.buckets.map((bucket) => ({ - x: bucket.key, - y: bucket.sessions.value ?? 0, - })) ?? [], - }, - requests: { - value: response.aggregations?.requests?.doc_count, - timeseries: - response.aggregations?.timeseries?.buckets.map((bucket) => ({ - x: bucket.key, - y: bucket.requests.doc_count ?? 0, - })) ?? [], - }, - maxLoadTime: { - value: response.aggregations?.maxLoadTime?.value, - timeseries: - response.aggregations?.timeseries?.buckets.map((bucket) => ({ - x: bucket.key, - y: bucket.maxLoadTime?.value ?? 0, - })) ?? [], - }, - crashCount: { - value: - response.aggregations?.crashCount?.doc_count ?? 0 / durationAsMinutes, - timeseries: - response.aggregations?.timeseries?.buckets.map((bucket) => ({ - x: bucket.key, - y: bucket.crashCount.doc_count ?? 0, - })) ?? [], - }, + currentPeriod, + previousPeriod, }; } diff --git a/x-pack/plugins/apm/server/routes/mobile/route.ts b/x-pack/plugins/apm/server/routes/mobile/route.ts index e18e7cae75c87..e8fe0515f11c8 100644 --- a/x-pack/plugins/apm/server/routes/mobile/route.ts +++ b/x-pack/plugins/apm/server/routes/mobile/route.ts @@ -11,12 +11,12 @@ import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { environmentRt, kueryRt, rangeRt } from '../default_api_types'; import { offsetRt } from '../../../common/comparison_rt'; import { - getHttpRequestsChart, + getMobileHttpRequests, HttpRequestsTimeseries, -} from './get_http_requests_chart'; +} from './get_mobile_http_requests'; import { getMobileFilters } from './get_mobile_filters'; -import { getSessionsChart, SessionsTimeseries } from './get_sessions_chart'; -import { getMobileStats, MobileStats } from './get_mobile_stats'; +import { getMobileSessions, SessionsTimeseries } from './get_mobile_sessions'; +import { getMobileStatsPeriods, MobilePeriodStats } from './get_mobile_stats'; const mobileFiltersRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/services/{serviceName}/mobile/filters', @@ -73,13 +73,13 @@ const mobileStatsRoute = createApmServerRoute({ ]), }), options: { tags: ['access:apm'] }, - handler: async (resources): Promise => { + handler: async (resources): Promise => { const apmEventClient = await getApmEventClient(resources); const { params } = resources; const { serviceName } = params.path; const { kuery, environment, start, end } = params.query; - const stats = await getMobileStats({ + const stats = await getMobileStatsPeriods({ kuery, environment, start, @@ -118,7 +118,7 @@ const sessionsChartRoute = createApmServerRoute({ const { kuery, environment, start, end, transactionName, offset } = params.query; - const { currentPeriod, previousPeriod } = await getSessionsChart({ + const { currentPeriod, previousPeriod } = await getMobileSessions({ kuery, environment, transactionName, @@ -159,7 +159,7 @@ const httpRequestsChartRoute = createApmServerRoute({ const { kuery, environment, start, end, transactionName, offset } = params.query; - const { currentPeriod, previousPeriod } = await getHttpRequestsChart({ + const { currentPeriod, previousPeriod } = await getMobileHttpRequests({ kuery, environment, transactionName, diff --git a/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule_template_metadata.ts b/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule_template_metadata.ts index d3bf7e0d58edd..aa6c8347b8215 100644 --- a/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule_template_metadata.ts +++ b/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule_template_metadata.ts @@ -12,6 +12,7 @@ export const cspRuleTemplateMetadataSchema = rt.object({ name: rt.string(), id: rt.string(), version: rt.string(), + rule_number: rt.maybe(rt.string()), }), default_value: rt.maybe(rt.string()), description: rt.string(), diff --git a/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts b/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts index d128713e6fdd6..a7f07f70fd89e 100644 --- a/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts +++ b/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts @@ -18,7 +18,7 @@ import { CLOUDBEAT_VANILLA, CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE, } from '../constants'; -import { BenchmarkId } from '../types'; +import type { BenchmarkId, Score } from '../types'; /** * @example @@ -72,3 +72,15 @@ export function assert(condition: any, msg?: string): asserts condition { throw new Error(msg); } } + +/** + * @param value value is [0, 1] range + */ +export const roundScore = (value: number): Score => Number((value * 100).toFixed(1)); + +export const calculatePostureScore = (passed: number, failed: number): Score => { + const total = passed + failed; + if (total === 0) return total; + + return roundScore(passed / (passed + failed)); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx b/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx new file mode 100644 index 0000000000000..cc5a6de2a6c53 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/compliance_score_bar.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip, useEuiTheme } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { calculatePostureScore } from '../../common/utils/helpers'; +import { statusColors } from '../common/constants'; + +export const ComplianceScoreBar = ({ + totalPassed, + totalFailed, +}: { + totalPassed: number; + totalFailed: number; +}) => { + const { euiTheme } = useEuiTheme(); + const complianceScore = calculatePostureScore(totalPassed, totalFailed); + + return ( + + + + + {!!totalFailed && ( + + )} + {!!totalPassed && ( + + )} + + + + + {`${complianceScore.toFixed(0)}%`} + + + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/risks_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/risks_table.tsx index 34fdff3eac69c..51f7847ffe5a1 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/risks_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/risks_table.tsx @@ -13,12 +13,9 @@ import { EuiFlexItem, EuiInMemoryTable, EuiLink, - EuiText, - EuiToolTip, - useEuiTheme, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { statusColors } from '../../../common/constants'; +import { ComplianceScoreBar } from '../../../components/compliance_score_bar'; import { ComplianceDashboardData, GroupedFindingsEvaluation } from '../../../../common/types'; export interface RisksTableProps { @@ -49,8 +46,6 @@ export const RisksTable = ({ viewAllButtonTitle, compact, }: RisksTableProps) => { - const { euiTheme } = useEuiTheme(); - const columns: Array> = useMemo( () => [ { @@ -76,63 +71,11 @@ export const RisksTable = ({ defaultMessage: 'Compliance', }), render: (postureScore: GroupedFindingsEvaluation['postureScore'], data) => ( - - - - - - - - - - - {`${ - postureScore?.toFixed(0) || 0 - }%`} - - + ), }, ], - [ - compact, - euiTheme.border.radius.medium, - euiTheme.font.weight.bold, - euiTheme.size.s, - euiTheme.size.xs, - onCellClick, - ] + [compact, onCellClick] ); const sortedByComplianceScore = getTopRisks(cisSectionsEvaluations, maxItems); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/__mocks__/findings.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/__mocks__/findings.ts index 48956856bf31c..274d5a789b981 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/__mocks__/findings.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/__mocks__/findings.ts @@ -89,6 +89,7 @@ export const mockFindingsHit: CspFinding = { 'Kubernetes provides a `default` service account which is used by cluster workloads where no specific service account is assigned to the pod. Where access to the Kubernetes API from a pod is required, a specific service account should be created for that pod, and rights granted to that service account. The default service account should be configured such that it does not provide a service account token and does not have any explicit rights assignments.\n', version: '1.0', benchmark: { + rule_number: '1.1.1', name: 'CIS Kubernetes V1.23', id: 'cis_k8s', version: 'v1.0.0', diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.test.tsx index 56b7c624287e9..f082171b22da1 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.test.tsx @@ -32,6 +32,7 @@ const getFakeFindings = (name: string): CspFinding & { id: string } => ({ rule: { audit: chance.paragraph(), benchmark: { + rule_number: '1.1.1', name: 'CIS Kubernetes', version: '1.6.0', id: 'cis_k8s', @@ -140,12 +141,11 @@ describe('', () => { const row = data[0]; const columns = [ - 'resource.id', 'result.evaluation', - 'resource.sub_type', + 'resource.id', 'resource.name', + 'resource.sub_type', 'rule.name', - 'cluster_id', ]; columns.forEach((field) => { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.tsx index 12b26f1b94bd1..34f29fa6473a2 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings/latest_findings_table.tsx @@ -64,14 +64,13 @@ const FindingsTableComponent = ({ ] = useMemo( () => [ getExpandColumn({ onClick: setSelectedFinding }), - createColumnWithFilters(baseFindingsColumns['resource.id'], { onAddFilter }), createColumnWithFilters(baseFindingsColumns['result.evaluation'], { onAddFilter }), - createColumnWithFilters(baseFindingsColumns['resource.sub_type'], { onAddFilter }), + createColumnWithFilters(baseFindingsColumns['resource.id'], { onAddFilter }), createColumnWithFilters(baseFindingsColumns['resource.name'], { onAddFilter }), + createColumnWithFilters(baseFindingsColumns['resource.sub_type'], { onAddFilter }), + baseFindingsColumns['rule.benchmark.rule_number'], createColumnWithFilters(baseFindingsColumns['rule.name'], { onAddFilter }), - createColumnWithFilters(baseFindingsColumns['rule.benchmark.name'], { onAddFilter }), baseFindingsColumns['rule.section'], - createColumnWithFilters(baseFindingsColumns.cluster_id, { onAddFilter }), baseFindingsColumns['@timestamp'], ], [onAddFilter] diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_container.tsx index 10f2ef8dcec2f..9964077f42f44 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_container.tsx @@ -42,7 +42,7 @@ const getDefaultQuery = ({ query, filters, pageIndex: 0, - sortDirection: 'desc', + sortDirection: 'asc', }); export const FindingsByResourceContainer = ({ dataView }: FindingsBaseProps) => ( @@ -179,7 +179,7 @@ const LatestFindingsByResource = ({ dataView }: FindingsBaseProps) => { }); }} sorting={{ - sort: { field: 'failed_findings', direction: urlQuery.sortDirection }, + sort: { field: 'compliance_score', direction: urlQuery.sortDirection }, }} onAddFilter={(field, value, negate) => setUrlQuery({ diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx index f3f05fcc9f9f0..a98cedafc6610 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.test.tsx @@ -7,20 +7,19 @@ import React from 'react'; import { render, screen, within } from '@testing-library/react'; import * as TEST_SUBJECTS from '../test_subjects'; -import { FindingsByResourceTable, formatNumber, getResourceId } from './findings_by_resource_table'; +import { FindingsByResourceTable, getResourceId } from './findings_by_resource_table'; import type { PropsOf } from '@elastic/eui'; import Chance from 'chance'; -import numeral from '@elastic/numeral'; import { TestProvider } from '../../../test/test_provider'; import type { FindingsByResourcePage } from './use_findings_by_resource'; +import { calculatePostureScore } from '../../../../common/utils/helpers'; const chance = new Chance(); const getFakeFindingsByResource = (): FindingsByResourcePage => { - const count = chance.integer(); - const total = chance.integer() + count + 1; - const normalized = count / total; - + const failed = chance.natural(); + const passed = chance.natural(); + const total = failed + passed; const [resourceName, resourceSubtype, ruleBenchmarkName, ...cisSections] = chance.unique( chance.word, 5 @@ -33,9 +32,11 @@ const getFakeFindingsByResource = (): FindingsByResourcePage => { 'resource.sub_type': resourceSubtype, 'rule.section': cisSections, 'rule.benchmark.name': ruleBenchmarkName, - failed_findings: { - count, - normalized, + compliance_score: passed / total, + findings: { + failed_findings: failed, + passed_findings: passed, + normalized: passed / total, total_findings: total, }, }; @@ -50,7 +51,7 @@ describe('', () => { items: [], pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 0 }, sorting: { - sort: { field: 'failed_findings', direction: 'desc' }, + sort: { field: 'compliance_score', direction: 'desc' }, }, setTableOptions: jest.fn(), onAddFilter: jest.fn(), @@ -75,7 +76,7 @@ describe('', () => { items: data, pagination: { pageIndex: 0, pageSize: 10, totalItemCount: 0 }, sorting: { - sort: { field: 'failed_findings', direction: 'desc' }, + sort: { field: 'compliance_score', direction: 'desc' }, }, setTableOptions: jest.fn(), onAddFilter: jest.fn(), @@ -97,10 +98,13 @@ describe('', () => { expect(within(row).getByText(item['resource.name'])).toBeInTheDocument(); if (item['resource.sub_type']) expect(within(row).getByText(item['resource.sub_type'])).toBeInTheDocument(); - expect(within(row).getByText(item['rule.section'].join(', '))).toBeInTheDocument(); - expect(within(row).getByText(formatNumber(item.failed_findings.count))).toBeInTheDocument(); expect( - within(row).getByText(new RegExp(numeral(item.failed_findings.normalized).format('0%'))) + within(row).getByText( + `${calculatePostureScore( + item.findings.passed_findings, + item.findings.failed_findings + ).toFixed(0)}%` + ) ).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.tsx index 18ddb34256df9..630edd6cb8dcf 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/findings_by_resource_table.tsx @@ -8,17 +8,16 @@ import React, { useMemo } from 'react'; import { EuiEmptyPrompt, EuiBasicTable, - EuiTextColor, type EuiTableFieldDataColumnType, type CriteriaWithPagination, type Pagination, EuiToolTip, EuiBasicTableProps, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import numeral from '@elastic/numeral'; import { Link, generatePath } from 'react-router-dom'; +import { ComplianceScoreBar } from '../../../components/compliance_score_bar'; import * as TEST_SUBJECTS from '../test_subjects'; import type { FindingsByResourcePage } from './use_findings_by_resource'; import { findingsNavigation } from '../../../common/navigation/constants'; @@ -31,9 +30,7 @@ import { export const formatNumber = (value: number) => value < 1000 ? value : numeral(value).format('0.0a'); -type Sorting = Required< - EuiBasicTableProps> ->['sorting']; +type Sorting = Required>['sorting']; interface Props { items: FindingsByResourcePage[]; @@ -66,9 +63,8 @@ const FindingsByResourceTableComponent = ({ createColumnWithFilters(findingsByResourceColumns['resource.sub_type'], { onAddFilter }), createColumnWithFilters(findingsByResourceColumns['resource.name'], { onAddFilter }), createColumnWithFilters(findingsByResourceColumns['rule.benchmark.name'], { onAddFilter }), - findingsByResourceColumns['rule.section'], createColumnWithFilters(findingsByResourceColumns.cluster_id, { onAddFilter }), - findingsByResourceColumns.failed_findings, + findingsByResourceColumns.compliance_score, ], [onAddFilter] ); @@ -106,6 +102,7 @@ const baseColumns: Array> = { ...baseFindingsColumns['resource.id'], field: 'resource_id', + width: '15%', render: (resourceId: FindingsByResourcePage['resource_id']) => ( > = }, baseFindingsColumns.cluster_id, { - field: 'failed_findings', + field: 'compliance_score', width: '150px', truncateText: true, sortable: true, name: ( ), - render: (failedFindings: FindingsByResourcePage['failed_findings']) => ( - - <> - - {formatNumber(failedFindings.count)} - - ({numeral(failedFindings.normalized).format('0%')}) - - + render: (complianceScore: FindingsByResourcePage['compliance_score'], data) => ( + ), dataType: 'number', }, diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.tsx index 2de252b5515e0..41f18af723b8e 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/resource_findings/resource_findings_table.tsx @@ -57,8 +57,8 @@ const ResourceFindingsTableComponent = ({ () => [ getExpandColumn({ onClick: setSelectedFinding }), createColumnWithFilters(baseFindingsColumns['result.evaluation'], { onAddFilter }), + baseFindingsColumns['rule.benchmark.rule_number'], createColumnWithFilters(baseFindingsColumns['rule.name'], { onAddFilter }), - createColumnWithFilters(baseFindingsColumns['rule.benchmark.name'], { onAddFilter }), baseFindingsColumns['rule.section'], baseFindingsColumns['@timestamp'], ], diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts index 139013d88cec4..4a0110e0d536e 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/latest_findings_by_resource/use_findings_by_resource.ts @@ -9,12 +9,12 @@ import { lastValueFrom } from 'rxjs'; import { IKibanaSearchRequest, IKibanaSearchResponse } from '@kbn/data-plugin/common'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { Pagination } from '@elastic/eui'; +import { MAX_FINDINGS_TO_LOAD } from '../../../common/constants'; import { useKibana } from '../../../common/hooks/use_kibana'; import { showErrorToast } from '../latest_findings/use_latest_findings'; import type { FindingsBaseEsQuery, Sort } from '../types'; import { getAggregationCount, getFindingsCountAggQuery } from '../utils/utils'; import { CSP_LATEST_FINDINGS_DATA_VIEW } from '../../../../common/constants'; -import { MAX_FINDINGS_TO_LOAD } from '../../../common/constants'; interface UseFindingsByResourceOptions extends FindingsBaseEsQuery { enabled: boolean; @@ -35,11 +35,13 @@ type FindingsAggResponse = IKibanaSearchResponse< >; export interface FindingsByResourcePage { - failed_findings: { - count: number; + findings: { + failed_findings: number; + passed_findings: number; normalized: number; total_findings: number; }; + compliance_score: number; resource_id: string; cluster_id: string; 'resource.name': string; @@ -56,6 +58,8 @@ interface FindingsByResourceAggs { interface FindingsAggBucket extends estypes.AggregationsStringRareTermsBucketKeys { failed_findings: estypes.AggregationsMultiBucketBase; + compliance_score: estypes.AggregationsScriptedMetricAggregate; + passed_findings: estypes.AggregationsMultiBucketBase; name: estypes.AggregationsMultiBucketAggregateBase; subtype: estypes.AggregationsMultiBucketAggregateBase; cluster_id: estypes.AggregationsMultiBucketAggregateBase; @@ -92,15 +96,27 @@ export const getFindingsByResourceAggQuery = ({ failed_findings: { filter: { term: { 'result.evaluation': 'failed' } }, }, + passed_findings: { + filter: { term: { 'result.evaluation': 'passed' } }, + }, cluster_id: { terms: { field: 'cluster_id', size: 1 }, }, - sort_failed_findings: { + compliance_score: { + bucket_script: { + buckets_path: { + passed: 'passed_findings>_count', + failed: 'failed_findings>_count', + }, + script: 'params.passed / (params.passed + params.failed)', + }, + }, + sort_by_compliance_score: { bucket_sort: { size: MAX_FINDINGS_TO_LOAD, sort: [ { - 'failed_findings>_count': { order: sortDirection }, + compliance_score: { order: sortDirection }, _count: { order: 'desc' }, _key: { order: 'asc' }, }, @@ -177,11 +193,13 @@ const createFindingsByResource = (resource: FindingsAggBucket): FindingsByResour cluster_id: resource.cluster_id.buckets[0]?.key, ['rule.section']: resource.cis_sections.buckets.map((v) => v.key), ['rule.benchmark.name']: resource.benchmarkName.buckets[0]?.key, - failed_findings: { - count: resource.failed_findings.doc_count, + compliance_score: resource.compliance_score.value, + findings: { + failed_findings: resource.failed_findings.doc_count, normalized: resource.doc_count > 0 ? resource.failed_findings.doc_count / resource.doc_count : 0, total_findings: resource.doc_count, + passed_findings: resource.passed_findings.doc_count, }, }; }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_layout.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_layout.tsx index e279d78ac0b7c..1112d1f9d3688 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_layout.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/layout/findings_layout.tsx @@ -123,9 +123,10 @@ const baseColumns = [ }, { field: 'rule.name', - name: i18n.translate('xpack.csp.findings.findingsTable.findingsTableColumn.ruleColumnLabel', { - defaultMessage: 'Rule', - }), + name: i18n.translate( + 'xpack.csp.findings.findingsTable.findingsTableColumn.ruleNameColumnLabel', + { defaultMessage: 'Rule Name' } + ), sortable: true, render: (name: string) => ( @@ -134,12 +135,29 @@ const baseColumns = [ ), }, { - field: 'rule.benchmark.name', + field: 'rule.benchmark.rule_number', name: i18n.translate( - 'xpack.csp.findings.findingsTable.findingsTableColumn.ruleBenchmarkColumnLabel', - { defaultMessage: 'Benchmark' } + 'xpack.csp.findings.findingsTable.findingsTableColumn.ruleNumberColumnLabel', + { + defaultMessage: 'Rule Number', + } + ), + width: '120px', + }, + { + field: 'rule.benchmark.name', + name: ( + ), - width: '10%', sortable: true, truncateText: true, }, @@ -149,7 +167,6 @@ const baseColumns = [ 'xpack.csp.findings.findingsTable.findingsTableColumn.ruleSectionColumnLabel', { defaultMessage: 'CIS Section' } ), - width: '7%', sortable: true, truncateText: true, render: (section: string) => ( @@ -164,15 +181,14 @@ const baseColumns = [ ), - width: '150px', sortable: true, truncateText: true, render: (section: string) => ( @@ -183,6 +199,7 @@ const baseColumns = [ }, { field: '@timestamp', + align: 'right', width: '10%', name: i18n.translate( 'xpack.csp.findings.findingsTable.findingsTableColumn.lastCheckedColumnLabel', diff --git a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/accounts_stats_collector.ts b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/accounts_stats_collector.ts index 01637f43a5dea..cf28f5b07113d 100644 --- a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/accounts_stats_collector.ts +++ b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/accounts_stats_collector.ts @@ -7,7 +7,7 @@ import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import type { Logger } from '@kbn/core/server'; import type { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; -import { calculatePostureScore } from '../../../routes/compliance_dashboard/get_stats'; +import { calculatePostureScore } from '../../../../common/utils/helpers'; import type { CspmAccountsStats } from './types'; import { LATEST_FINDINGS_INDEX_DEFAULT_NS } from '../../../../common/constants'; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_grouped_findings_evaluation.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_grouped_findings_evaluation.ts index 4f295ad7a2fdb..f20b43619e914 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_grouped_findings_evaluation.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_grouped_findings_evaluation.ts @@ -11,7 +11,7 @@ import type { QueryDslQueryContainer, SearchRequest, } from '@elastic/elasticsearch/lib/api/types'; -import { calculatePostureScore } from './get_stats'; +import { calculatePostureScore } from '../../../common/utils/helpers'; import type { ComplianceDashboardData } from '../../../common/types'; import { KeyDocCount } from './compliance_dashboard'; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.test.ts index aff4e39f9d49c..1a824e1671227 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.test.ts @@ -5,12 +5,8 @@ * 2.0. */ -import { - calculatePostureScore, - FindingsEvaluationsQueryResult, - getStatsFromFindingsEvaluationsAggs, - roundScore, -} from './get_stats'; +import { FindingsEvaluationsQueryResult, getStatsFromFindingsEvaluationsAggs } from './get_stats'; +import { calculatePostureScore, roundScore } from '../../../common/utils/helpers'; const standardQueryResult: FindingsEvaluationsQueryResult = { resources_evaluated: { diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.ts index d0fcd5b796774..a59d7487488e0 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.ts @@ -7,15 +7,8 @@ import { ElasticsearchClient } from '@kbn/core/server'; import type { QueryDslQueryContainer, SearchRequest } from '@elastic/elasticsearch/lib/api/types'; -import type { ComplianceDashboardData, Score } from '../../../common/types'; - -/** - * @param value value is [0, 1] range - */ -export const roundScore = (value: number): Score => Number((value * 100).toFixed(1)); - -export const calculatePostureScore = (passed: number, failed: number): Score => - roundScore(passed / (passed + failed)); +import { calculatePostureScore } from '../../../common/utils/helpers'; +import type { ComplianceDashboardData } from '../../../common/types'; export interface FindingsEvaluationsQueryResult { failed_findings: { diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_trends.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_trends.ts index a47b63e1fb921..cc8234fa6d7af 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_trends.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_trends.ts @@ -6,9 +6,9 @@ */ import { ElasticsearchClient } from '@kbn/core/server'; +import { calculatePostureScore } from '../../../common/utils/helpers'; import { BENCHMARK_SCORE_INDEX_DEFAULT_NS } from '../../../common/constants'; import type { PosturePolicyTemplate, Stats } from '../../../common/types'; -import { calculatePostureScore } from './get_stats'; export interface ScoreTrendDoc { '@timestamp': string; diff --git a/x-pack/plugins/cloud_security_posture/server/saved_objects/migrations/csp_rule_template.ts b/x-pack/plugins/cloud_security_posture/server/saved_objects/migrations/csp_rule_template.ts index 470ec21cddc7b..10f693e9ea7c3 100644 --- a/x-pack/plugins/cloud_security_posture/server/saved_objects/migrations/csp_rule_template.ts +++ b/x-pack/plugins/cloud_security_posture/server/saved_objects/migrations/csp_rule_template.ts @@ -28,7 +28,7 @@ function migrateCspRuleTemplatesToV840( muted, metadata: { ...metadata, - benchmark: { ...benchmark, id: 'cis_k8s' }, + benchmark: { ...benchmark, id: 'cis_k8s', rule_number: '' }, impact: metadata.impact || undefined, default_value: metadata.default_value || undefined, references: metadata.references || undefined, diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 696e05e9c18bb..bb43d32b6cfaf 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -186,7 +186,7 @@ export const getAgentsHandler: RequestHandler< kuery: request.query.kuery, sortField: request.query.sortField, sortOrder: request.query.sortOrder, - getTotalInactive: true, + getTotalInactive: request.query.showInactive, }); const { total, page, perPage, totalInactive = 0 } = agentRes; diff --git a/x-pack/plugins/fleet/server/services/agents/action_runner.ts b/x-pack/plugins/fleet/server/services/agents/action_runner.ts index cba8f677c8b0f..ce338b3d637f3 100644 --- a/x-pack/plugins/fleet/server/services/agents/action_runner.ts +++ b/x-pack/plugins/fleet/server/services/agents/action_runner.ts @@ -6,7 +6,6 @@ */ import { v4 as uuidv4 } from 'uuid'; -import type { SortResults } from '@elastic/elasticsearch/lib/api/types'; import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import { withSpan } from '@kbn/apm-utils'; @@ -21,8 +20,8 @@ import { SO_SEARCH_LIMIT } from '../../../common/constants'; import { getAgentActions } from './actions'; import { closePointInTime, getAgentsByKuery } from './crud'; import type { BulkActionsResolver } from './bulk_actions_resolver'; - -export const MAX_RETRY_COUNT = 5; +import type { RetryParams } from './retry_helper'; +import { getRetryParams, MAX_RETRY_COUNT } from './retry_helper'; export interface ActionParams { kuery: string; @@ -34,13 +33,6 @@ export interface ActionParams { [key: string]: any; } -export interface RetryParams { - pitId: string; - searchAfter?: SortResults; - retryCount?: number; - taskId?: string; -} - export abstract class ActionRunner { protected esClient: ElasticsearchClient; protected soClient: SavedObjectsClientContract; @@ -79,7 +71,9 @@ export abstract class ActionRunner { appContextService .getLogger() .info( - `Running action asynchronously, actionId: ${this.actionParams.actionId}, total agents: ${this.actionParams.total}` + `Running action asynchronously, actionId: ${this.actionParams.actionId}${ + this.actionParams.total ? ', total agents:' + this.actionParams.total : '' + }` ); if (!this.bulkActionsResolver) { @@ -153,10 +147,12 @@ export abstract class ActionRunner { this.actionParams.actionId!, this.getTaskType() + ':check' ); + const retryParams: RetryParams = getRetryParams(this.getTaskType(), this.retryParams); + return await this.bulkActionsResolver!.run( this.actionParams, { - ...this.retryParams, + ...retryParams, retryCount: 1, }, this.getTaskType(), @@ -233,7 +229,9 @@ export abstract class ActionRunner { } } - await closePointInTime(this.esClient, pitId!); + if (pitId) { + await closePointInTime(this.esClient, pitId!); + } appContextService .getLogger() diff --git a/x-pack/plugins/fleet/server/services/agents/bulk_action_types.ts b/x-pack/plugins/fleet/server/services/agents/bulk_action_types.ts new file mode 100644 index 0000000000000..44ab84e0bc72d --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/bulk_action_types.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum BulkActionTaskType { + REASSIGN_RETRY = 'fleet:reassign_action:retry', + UNENROLL_RETRY = 'fleet:unenroll_action:retry', + UPGRADE_RETRY = 'fleet:upgrade_action:retry', + UPDATE_AGENT_TAGS_RETRY = 'fleet:update_agent_tags:retry', + REQUEST_DIAGNOSTICS_RETRY = 'fleet:request_diagnostics:retry', +} diff --git a/x-pack/plugins/fleet/server/services/agents/bulk_actions_resolver.ts b/x-pack/plugins/fleet/server/services/agents/bulk_actions_resolver.ts index b6ec1d082b39a..b68bae611252a 100644 --- a/x-pack/plugins/fleet/server/services/agents/bulk_actions_resolver.ts +++ b/x-pack/plugins/fleet/server/services/agents/bulk_actions_resolver.ts @@ -19,16 +19,11 @@ import { ReassignActionRunner } from './reassign_action_runner'; import { UpgradeActionRunner } from './upgrade_action_runner'; import { UpdateAgentTagsActionRunner } from './update_agent_tags_action_runner'; import { UnenrollActionRunner } from './unenroll_action_runner'; -import type { ActionParams, RetryParams } from './action_runner'; +import type { ActionParams } from './action_runner'; import { RequestDiagnosticsActionRunner } from './request_diagnostics_action_runner'; - -export enum BulkActionTaskType { - REASSIGN_RETRY = 'fleet:reassign_action:retry', - UNENROLL_RETRY = 'fleet:unenroll_action:retry', - UPGRADE_RETRY = 'fleet:upgrade_action:retry', - UPDATE_AGENT_TAGS_RETRY = 'fleet:update_agent_tags:retry', - REQUEST_DIAGNOSTICS_RETRY = 'fleet:request_diagnostics:retry', -} +import type { RetryParams } from './retry_helper'; +import { getRetryParams } from './retry_helper'; +import { BulkActionTaskType } from './bulk_action_types'; /** * Create and run retry tasks of agent bulk actions @@ -114,11 +109,7 @@ export class BulkActionsResolver { scope: ['fleet'], state: {}, params: { actionParams, retryParams }, - runAt: - runAt ?? - moment(new Date()) - .add(Math.pow(3, retryParams.retryCount ?? 1), 's') - .toDate(), + runAt: runAt ?? moment(new Date()).add(3, 's').toDate(), }); appContextService.getLogger().info('Scheduling task ' + taskId); return taskId; @@ -146,7 +137,10 @@ export function createRetryTask( const { esClient, soClient } = await getDeps(); - const retryParams = taskInstance.params.retryParams; + const retryParams: RetryParams = getRetryParams( + taskInstance.taskType, + taskInstance.params.retryParams + ); appContextService .getLogger() diff --git a/x-pack/plugins/fleet/server/services/agents/reassign_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/reassign_action_runner.ts index cf6e3988e7bef..b03146ab6b387 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign_action_runner.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign_action_runner.ts @@ -18,7 +18,7 @@ import { ActionRunner } from './action_runner'; import { bulkUpdateAgents } from './crud'; import { createErrorActionResults, createAgentAction } from './actions'; import { getHostedPolicies, isHostedAgent } from './hosted_agent'; -import { BulkActionTaskType } from './bulk_actions_resolver'; +import { BulkActionTaskType } from './bulk_action_types'; export class ReassignActionRunner extends ActionRunner { protected async processAgents(agents: Agent[]): Promise<{ actionId: string }> { diff --git a/x-pack/plugins/fleet/server/services/agents/request_diagnostics_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/request_diagnostics_action_runner.ts index 9728afedbb028..d9552f3a1a41e 100644 --- a/x-pack/plugins/fleet/server/services/agents/request_diagnostics_action_runner.ts +++ b/x-pack/plugins/fleet/server/services/agents/request_diagnostics_action_runner.ts @@ -12,7 +12,7 @@ import type { Agent } from '../../types'; import { ActionRunner } from './action_runner'; import { createAgentAction } from './actions'; -import { BulkActionTaskType } from './bulk_actions_resolver'; +import { BulkActionTaskType } from './bulk_action_types'; export class RequestDiagnosticsActionRunner extends ActionRunner { protected async processAgents(agents: Agent[]): Promise<{ actionId: string }> { diff --git a/x-pack/plugins/fleet/server/services/agents/retry_helper.ts b/x-pack/plugins/fleet/server/services/agents/retry_helper.ts new file mode 100644 index 0000000000000..b15d5faa87c44 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/retry_helper.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SortResults } from '@elastic/elasticsearch/lib/api/types'; + +import { BulkActionTaskType } from './bulk_action_types'; + +export const MAX_RETRY_COUNT = 20; + +export interface RetryParams { + pitId?: string; + searchAfter?: SortResults; + retryCount?: number; + taskId?: string; +} + +export function getRetryParams(taskType: string, retryParams: RetryParams): RetryParams { + // update tags will retry with tags filter + return taskType === BulkActionTaskType.UPDATE_AGENT_TAGS_RETRY + ? { + ...retryParams, + pitId: undefined, + searchAfter: undefined, + } + : retryParams; +} diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/unenroll_action_runner.ts index 359b016bd1b8c..8f9a7f3763cc0 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll_action_runner.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll_action_runner.ts @@ -29,7 +29,7 @@ import { getUnenrollAgentActions, } from './actions'; import { getHostedPolicies, isHostedAgent } from './hosted_agent'; -import { BulkActionTaskType } from './bulk_actions_resolver'; +import { BulkActionTaskType } from './bulk_action_types'; export class UnenrollActionRunner extends ActionRunner { protected async processAgents(agents: Agent[]): Promise<{ actionId: string }> { diff --git a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts index d59aee0c700b1..30c56b53640af 100644 --- a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts @@ -8,9 +8,12 @@ import type { SavedObjectsClientContract } from '@kbn/core/server'; import type { ElasticsearchClientMock } from '@kbn/core/server/mocks'; import { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks'; +import type { Agent } from '../../types'; + import { createClientMock } from './action.mock'; +import { MAX_RETRY_COUNT } from './retry_helper'; import { updateAgentTags } from './update_agent_tags'; -import { updateTagsBatch } from './update_agent_tags_action_runner'; +import { UpdateAgentTagsActionRunner, updateTagsBatch } from './update_agent_tags_action_runner'; jest.mock('../app_context', () => { return { @@ -74,6 +77,7 @@ describe('update_agent_tags', () => { esClient.updateByQuery.mockResolvedValue({ failures: [], updated: 1 } as any); mockRunAsync.mockClear(); + (UpdateAgentTagsActionRunner as jest.Mock).mockClear(); }); it('should remove duplicate tags', async () => { @@ -83,7 +87,9 @@ describe('update_agent_tags', () => { expect.objectContaining({ conflicts: 'proceed', index: '.fleet-agents', - query: { terms: { _id: ['agent1'] } }, + query: { + terms: { _id: ['agent1'] }, + }, script: expect.objectContaining({ lang: 'painless', params: expect.objectContaining({ @@ -121,37 +127,16 @@ describe('update_agent_tags', () => { expect(actionResults.body[1].error).not.toBeDefined(); }); - it('should update action results on success - kuery', async () => { - await updateTagsBatch( - soClient, - esClient, - [], - {}, - { - tagsToAdd: ['new'], - tagsToRemove: [], - kuery: '', - } - ); - - const actionResults = esClient.bulk.mock.calls[0][0] as any; - const agentIds = actionResults?.body - ?.filter((i: any) => i.agent_id) - .map((i: any) => i.agent_id); - expect(agentIds[0]).toHaveLength(36); // uuid - expect(actionResults.body[1].error).not.toBeDefined(); - }); - it('should skip hosted agent from total when agentIds are passed', async () => { - const { esClient: esClientMock, agentInHostedDoc } = createClientMock(); + const { esClient: esClientMock, agentInHostedDoc, agentInRegularDoc } = createClientMock(); esClientMock.updateByQuery.mockReset(); - esClientMock.updateByQuery.mockResolvedValue({ failures: [], updated: 0, total: 0 } as any); + esClientMock.updateByQuery.mockResolvedValue({ failures: [], updated: 1, total: 1 } as any); await updateAgentTags( soClient, esClientMock, - { agentIds: [agentInHostedDoc._id] }, + { agentIds: [agentInHostedDoc._id, agentInRegularDoc._id] }, ['newName'], [] ); @@ -160,9 +145,9 @@ describe('update_agent_tags', () => { expect(agentAction?.body).toEqual( expect.objectContaining({ action_id: expect.anything(), - agents: [], + agents: [agentInRegularDoc._id], type: 'UPDATE_TAGS', - total: 0, + total: 1, }) ); }); @@ -205,14 +190,14 @@ describe('update_agent_tags', () => { updateTagsBatch( soClient, esClient, - [], + [{ id: 'agent1' } as Agent], {}, { tagsToAdd: ['new'], tagsToRemove: [], kuery: '', total: 100, - retryCount: 5, + retryCount: MAX_RETRY_COUNT, } ) ).rejects.toThrowError('version conflict of 100 agents'); @@ -247,13 +232,24 @@ describe('update_agent_tags', () => { await updateAgentTags(soClient, esClient, { kuery: '', batchSize: 2 }, ['newName'], []); expect(mockRunAsync).toHaveBeenCalled(); + expect(UpdateAgentTagsActionRunner).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ + batchSize: 2, + kuery: '(NOT (tags:newName))', + tagsToAdd: ['newName'], + tagsToRemove: [], + }), + expect.anything() + ); }); it('should add tags filter if only one tag to add', async () => { await updateTagsBatch( soClient, esClient, - [], + [{ id: 'agent1' } as Agent, { id: 'agent2' } as Agent], {}, { tagsToAdd: ['new'], @@ -263,94 +259,57 @@ describe('update_agent_tags', () => { ); const updateByQuery = esClient.updateByQuery.mock.calls[0][0] as any; - expect(updateByQuery.query).toMatchInlineSnapshot(` - Object { - "bool": Object { - "filter": Array [ - Object { - "bool": Object { - "must_not": Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "match": Object { - "status": "inactive", - }, - }, - ], - }, - }, - Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "match": Object { - "status": "unenrolled", - }, - }, - ], - }, - }, - ], - }, - }, - }, - }, - Object { - "bool": Object { - "must_not": Object { - "bool": Object { - "minimum_should_match": 1, - "should": Array [ - Object { - "match": Object { - "tags": "new", - }, - }, - ], - }, - }, - }, - }, - ], - }, - } - `); + expect(updateByQuery.query).toEqual({ + terms: { _id: ['agent1', 'agent2'] }, + }); }); it('should add tags filter if only one tag to remove', async () => { - await updateTagsBatch( + await updateAgentTags(soClient, esClient, { kuery: '' }, [], ['remove']); + + expect(UpdateAgentTagsActionRunner).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ + batchSize: 10000, + kuery: '(tags:remove)', + tagsToAdd: [], + tagsToRemove: ['remove'], + }), + expect.anything() + ); + }); + + it('should add tags filter to existing kuery if only one tag to remove', async () => { + await updateAgentTags( soClient, esClient, + { kuery: 'status:healthy OR status:offline' }, [], - {}, - { - tagsToAdd: [], - tagsToRemove: ['remove'], - kuery: '', - } + ['remove'] ); - const updateByQuery = esClient.updateByQuery.mock.calls[0][0] as any; - expect(JSON.stringify(updateByQuery.query)).toContain( - '{"bool":{"should":[{"match":{"tags":"remove"}}],"minimum_should_match":1}}' + expect(UpdateAgentTagsActionRunner).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ + batchSize: 10000, + kuery: '(status:healthy OR status:offline) AND (tags:remove)', + tagsToAdd: [], + tagsToRemove: ['remove'], + }), + expect.anything() ); }); - it('should write total from updateByQuery result if query returns less results', async () => { + it('should write total from total param if updateByQuery returns less results', async () => { esClient.updateByQuery.mockReset(); esClient.updateByQuery.mockResolvedValue({ failures: [], updated: 0, total: 50 } as any); await updateTagsBatch( soClient, esClient, - [], + [{ id: 'agent1' } as Agent], {}, { tagsToAdd: ['new'], @@ -364,9 +323,9 @@ describe('update_agent_tags', () => { expect(agentAction?.body).toEqual( expect.objectContaining({ action_id: expect.anything(), - agents: [], + agents: ['agent1'], type: 'UPDATE_TAGS', - total: 50, + total: 100, }) ); }); diff --git a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts index a53a5f4536420..5e335bfd41996 100644 --- a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts +++ b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts @@ -10,7 +10,9 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/ import type { Agent } from '../../types'; import { AgentReassignmentError } from '../../errors'; -import { getAgentsById } from './crud'; +import { SO_SEARCH_LIMIT } from '../../constants'; + +import { getAgentsById, getAgentsByKuery, openPointInTime } from './crud'; import type { GetAgentsOptions } from '.'; import { UpdateAgentTagsActionRunner, updateTagsBatch } from './update_agent_tags_action_runner'; @@ -36,16 +38,41 @@ export async function updateAgentTags( } } } else if ('kuery' in options) { + const batchSize = options.batchSize ?? SO_SEARCH_LIMIT; + + const filters = []; + if (options.kuery !== '') { + filters.push(options.kuery); + } + if (tagsToAdd.length === 1 && tagsToRemove.length === 0) { + filters.push(`NOT (tags:${tagsToAdd[0]})`); + } else if (tagsToRemove.length === 1 && tagsToAdd.length === 0) { + filters.push(`tags:${tagsToRemove[0]}`); + } + + const kuery = filters.map((filter) => `(${filter})`).join(' AND '); + const pitId = await openPointInTime(esClient); + + // calculate total count + const res = await getAgentsByKuery(esClient, soClient, { + kuery, + showInactive: options.showInactive ?? false, + perPage: 0, + pitId, + }); + return await new UpdateAgentTagsActionRunner( esClient, soClient, { ...options, - kuery: options.kuery, + kuery, tagsToAdd, tagsToRemove, + batchSize, + total: res.total, }, - { pitId: '' } + { pitId } ).runActionAsyncWithRetry(); } diff --git a/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts index 9c87259452493..1f12968b67c36 100644 --- a/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts +++ b/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts @@ -8,24 +8,19 @@ import type { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server'; import { v4 as uuidv4 } from 'uuid'; import { uniq } from 'lodash'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { Agent } from '../../types'; -import { AGENTS_INDEX, AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../constants'; +import { AGENTS_INDEX } from '../../constants'; import { appContextService } from '../app_context'; -import { agentPolicyService } from '../agent_policy'; +import { ActionRunner } from './action_runner'; -import { SO_SEARCH_LIMIT } from '../../../common/constants'; - -import { ActionRunner, MAX_RETRY_COUNT } from './action_runner'; - -import { BulkActionTaskType } from './bulk_actions_resolver'; +import { BulkActionTaskType } from './bulk_action_types'; import { filterHostedPolicies } from './filter_hosted_agents'; import { bulkCreateAgentActionResults, createAgentAction } from './actions'; -import { getElasticsearchQuery } from './crud'; +import { MAX_RETRY_COUNT } from './retry_helper'; export class UpdateAgentTagsActionRunner extends ActionRunner { protected async processAgents(agents: Agent[]): Promise<{ actionId: string }> { @@ -50,26 +45,6 @@ export class UpdateAgentTagsActionRunner extends ActionRunner { protected getActionType() { return 'UPDATE_TAGS'; } - - async processAgentsInBatches(): Promise<{ actionId: string }> { - const { updated, took } = await updateTagsBatch( - this.soClient, - this.esClient, - [], - {}, - { - tagsToAdd: this.actionParams?.tagsToAdd, - tagsToRemove: this.actionParams?.tagsToRemove, - actionId: this.actionParams.actionId, - total: this.actionParams.total, - kuery: this.actionParams.kuery, - retryCount: this.retryParams.retryCount, - } - ); - - appContextService.getLogger().info(`processed ${updated} agents, took ${took}ms`); - return { actionId: this.actionParams.actionId! }; - } } export async function updateTagsBatch( @@ -97,38 +72,26 @@ export async function updateTagsBatch( ); const agentIds = filteredAgents.map((agent) => agent.id); - let query: estypes.QueryDslQueryContainer | undefined; - if (options.kuery !== undefined) { - const hostedPolicies = await agentPolicyService.list(soClient, { - kuery: `${AGENT_POLICY_SAVED_OBJECT_TYPE}.is_managed:true`, - perPage: SO_SEARCH_LIMIT, - }); - const hostedIds = hostedPolicies.items.map((item) => item.id); - - const extraFilters = []; - if (options.tagsToAdd.length === 1 && options.tagsToRemove.length === 0) { - extraFilters.push(`NOT (tags:${options.tagsToAdd[0]})`); - } else if (options.tagsToRemove.length === 1 && options.tagsToAdd.length === 0) { - extraFilters.push(`tags:${options.tagsToRemove[0]}`); - } - const DEFAULT_STATUS_FILTER = - 'status:online or (status:error or status:degraded) or (status:updating or status:unenrolling or status:enrolling) or status:offline'; - // removing default staus filters, as it is a runtime field and doesn't work with updateByQuery - // this is a quick fix for bulk update tags with default filters - const kuery = options.kuery === DEFAULT_STATUS_FILTER ? '' : options.kuery; - query = getElasticsearchQuery(kuery, false, false, hostedIds, extraFilters); - } else { - query = { - terms: { - _id: agentIds, - }, - }; + const actionId = options.actionId ?? uuidv4(); + if (agentIds.length === 0) { + appContextService.getLogger().debug('No agents to update tags, returning'); + return { actionId, updated: 0, took: 0 }; } + appContextService + .getLogger() + .debug( + `Agents to update tags in batch: ${agentIds.length}, tagsToAdd: ${options.tagsToAdd}, tagsToRemove: ${options.tagsToRemove}` + ); + let res; try { res = await esClient.updateByQuery({ - query, + query: { + terms: { + _id: agentIds, + }, + }, index: AGENTS_INDEX, refresh: true, wait_for_completion: true, @@ -166,16 +129,14 @@ export async function updateTagsBatch( appContextService.getLogger().debug(JSON.stringify(res).slice(0, 1000)); - const actionId = options.actionId ?? uuidv4(); - if (options.retryCount === undefined) { // creating an action doc so that update tags shows up in activity await createAgentAction(esClient, { id: actionId, - agents: options.kuery === undefined ? agentIds : [], + agents: agentIds, created_at: new Date().toISOString(), type: 'UPDATE_TAGS', - total: res.total, + total: options.total ?? res.total, }); } @@ -186,7 +147,7 @@ export async function updateTagsBatch( if (res.updated ?? 0 > 0) { await bulkCreateAgentActionResults( esClient, - (options.kuery === undefined ? agentIds : getUuidArray(res.updated!)).map((id) => ({ + agentIds.map((id) => ({ agentId: id, actionId, })) diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts index 501afe0d54043..ff677db23cacc 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts @@ -24,7 +24,7 @@ import type { GetAgentsOptions } from './crud'; import { bulkUpdateAgents } from './crud'; import { createErrorActionResults, createAgentAction } from './actions'; import { getHostedPolicies, isHostedAgent } from './hosted_agent'; -import { BulkActionTaskType } from './bulk_actions_resolver'; +import { BulkActionTaskType } from './bulk_action_types'; export class UpgradeActionRunner extends ActionRunner { protected async processAgents(agents: Agent[]): Promise<{ actionId: string }> { diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/time_shift.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/time_shift.tsx index 1d3cfefa66a4d..8bf26114416de 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/time_shift.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/time_shift.tsx @@ -151,10 +151,7 @@ export function TimeShift({ options={timeShiftOptions.filter(({ value }) => { const parsedValue = parseTimeShift(value); return ( - parsedValue && - !isValueTooSmall(parsedValue) && - !isValueNotMultiple(parsedValue) && - !(parsedValue === 'previous' && dateHistogramInterval.interval) + parsedValue && !isValueTooSmall(parsedValue) && !isValueNotMultiple(parsedValue) // && ); })} selectedOptions={getSelectedOption()} diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/calculations/counter_rate.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/calculations/counter_rate.tsx index 3b89de8e94c51..e516ce206eb87 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/calculations/counter_rate.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/calculations/counter_rate.tsx @@ -18,8 +18,7 @@ import { } from './utils'; import { DEFAULT_TIME_SCALE } from '../../time_scale_utils'; import { OperationDefinition } from '..'; -import { getFormatFromPreviousColumn, getFilter, combineErrorMessages } from '../helpers'; -import { getDisallowedPreviousShiftMessage } from '../../../time_shift_utils'; +import { getFormatFromPreviousColumn, getFilter } from '../helpers'; const ofName = buildLabelFunction((name?: string) => { return i18n.translate('xpack.lens.indexPattern.CounterRateOf', { @@ -107,16 +106,13 @@ export const counterRateOperation: OperationDefinition< return hasDateField(newIndexPattern); }, getErrorMessage: (layer: FormBasedLayer, columnId: string) => { - return combineErrorMessages([ - getErrorsForDateReference( - layer, - columnId, - i18n.translate('xpack.lens.indexPattern.counterRate', { - defaultMessage: 'Counter rate', - }) - ), - getDisallowedPreviousShiftMessage(layer, columnId), - ]); + return getErrorsForDateReference( + layer, + columnId, + i18n.translate('xpack.lens.indexPattern.counterRate', { + defaultMessage: 'Counter rate', + }) + ); }, getDisabledStatus(indexPattern, layer, layerType) { const opName = i18n.translate('xpack.lens.indexPattern.counterRate', { diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/calculations/cumulative_sum.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/calculations/cumulative_sum.tsx index 339f5e4bc4003..773f366990799 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/calculations/cumulative_sum.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/calculations/cumulative_sum.tsx @@ -17,8 +17,7 @@ import { checkForDataLayerType, } from './utils'; import { OperationDefinition } from '..'; -import { getFormatFromPreviousColumn, getFilter, combineErrorMessages } from '../helpers'; -import { getDisallowedPreviousShiftMessage } from '../../../time_shift_utils'; +import { getFormatFromPreviousColumn, getFilter } from '../helpers'; import { DOCUMENT_FIELD_NAME } from '../../../../../../common'; const ofName = buildLabelFunction((name?: string) => { @@ -107,16 +106,13 @@ export const cumulativeSumOperation: OperationDefinition< return true; }, getErrorMessage: (layer: FormBasedLayer, columnId: string) => { - return combineErrorMessages([ - getErrorsForDateReference( - layer, - columnId, - i18n.translate('xpack.lens.indexPattern.cumulativeSum', { - defaultMessage: 'Cumulative sum', - }) - ), - getDisallowedPreviousShiftMessage(layer, columnId), - ]); + return getErrorsForDateReference( + layer, + columnId, + i18n.translate('xpack.lens.indexPattern.cumulativeSum', { + defaultMessage: 'Cumulative sum', + }) + ); }, getDisabledStatus(indexPattern, layer, layerType) { const opName = i18n.translate('xpack.lens.indexPattern.cumulativeSum', { diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/calculations/differences.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/calculations/differences.tsx index f2c379b5af5c1..f6ce789859b8c 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/calculations/differences.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/calculations/differences.tsx @@ -17,8 +17,7 @@ import { checkForDataLayerType, } from './utils'; import { OperationDefinition } from '..'; -import { getFormatFromPreviousColumn, getFilter, combineErrorMessages } from '../helpers'; -import { getDisallowedPreviousShiftMessage } from '../../../time_shift_utils'; +import { getFormatFromPreviousColumn, getFilter } from '../helpers'; const OPERATION_NAME = 'differences'; @@ -93,16 +92,13 @@ export const derivativeOperation: OperationDefinition< return hasDateField(newIndexPattern); }, getErrorMessage: (layer: FormBasedLayer, columnId: string) => { - return combineErrorMessages([ - getErrorsForDateReference( - layer, - columnId, - i18n.translate('xpack.lens.indexPattern.derivative', { - defaultMessage: 'Differences', - }) - ), - getDisallowedPreviousShiftMessage(layer, columnId), - ]); + return getErrorsForDateReference( + layer, + columnId, + i18n.translate('xpack.lens.indexPattern.derivative', { + defaultMessage: 'Differences', + }) + ); }, getDisabledStatus(indexPattern, layer, layerType) { const opName = i18n.translate('xpack.lens.indexPattern.derivative', { diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/calculations/moving_average.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/calculations/moving_average.tsx index d7b46d09c0f1e..1d20f20521698 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/calculations/moving_average.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/calculations/moving_average.tsx @@ -21,14 +21,8 @@ import { checkForDataLayerType, } from './utils'; import { updateColumnParam } from '../../layer_helpers'; -import { - getFormatFromPreviousColumn, - isValidNumber, - getFilter, - combineErrorMessages, -} from '../helpers'; +import { getFormatFromPreviousColumn, isValidNumber, getFilter } from '../helpers'; import type { OperationDefinition, ParamEditorProps } from '..'; -import { getDisallowedPreviousShiftMessage } from '../../../time_shift_utils'; const ofName = buildLabelFunction((name?: string) => { return i18n.translate('xpack.lens.indexPattern.movingAverageOf', { @@ -115,16 +109,13 @@ export const movingAverageOperation: OperationDefinition< return hasDateField(newIndexPattern); }, getErrorMessage: (layer: FormBasedLayer, columnId: string) => { - return combineErrorMessages([ - getErrorsForDateReference( - layer, - columnId, - i18n.translate('xpack.lens.indexPattern.movingAverage', { - defaultMessage: 'Moving average', - }) - ), - getDisallowedPreviousShiftMessage(layer, columnId), - ]); + return getErrorsForDateReference( + layer, + columnId, + i18n.translate('xpack.lens.indexPattern.movingAverage', { + defaultMessage: 'Moving average', + }) + ); }, helpComponent: () => , helpComponentTitle: i18n.translate('xpack.lens.indexPattern.movingAverage.titleHelp', { diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/calculations/time_scale.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/calculations/time_scale.tsx index 59eb7fea46233..ecf91b9bd3609 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/calculations/time_scale.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/calculations/time_scale.tsx @@ -14,7 +14,6 @@ import { getErrorsForDateReference } from './utils'; import type { OperationDefinition } from '..'; import { combineErrorMessages, getFormatFromPreviousColumn } from '../helpers'; import { FormBasedLayer } from '../../../types'; -import { getDisallowedPreviousShiftMessage } from '../../../time_shift_utils'; type OverallMetricIndexPatternColumn = FormattedIndexPatternColumn & ReferenceBasedIndexPatternColumn & { @@ -110,7 +109,6 @@ export const timeScaleOperation: OperationDefinition combineErrorMessages([ getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), - getDisallowedPreviousShiftMessage(layer, columnId), getColumnReducedTimeRangeError(layer, columnId, indexPattern), ]), isTransferable: (column, newIndexPattern) => { diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/count.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/count.tsx index 60c1a0cdf0f5d..b5bd36d94225e 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/count.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/count.tsx @@ -23,7 +23,6 @@ import { isColumnOfType, } from './helpers'; import { adjustTimeScaleLabelSuffix } from '../time_scale_utils'; -import { getDisallowedPreviousShiftMessage } from '../../time_shift_utils'; import { updateColumnParam } from '../layer_helpers'; import { getColumnReducedTimeRangeError } from '../../reduced_time_range_utils'; import { getGroupByKey } from './get_group_by_key'; @@ -92,7 +91,6 @@ export const countOperation: OperationDefinition combineErrorMessages([ getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), - getDisallowedPreviousShiftMessage(layer, columnId), getColumnReducedTimeRangeError(layer, columnId, indexPattern), ]), allowAsReference: true, diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/editor/math_completion.ts b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/editor/math_completion.ts index a0a968a136886..11d6797a1c997 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/editor/math_completion.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/editor/math_completion.ts @@ -395,9 +395,9 @@ export async function getNamedArgumentSuggestions({ if (dateHistogramInterval == null) return true; const parsedValue = parseTimeShift(value); return ( - parsedValue !== 'previous' && - (parsedValue === 'invalid' || - Number.isInteger(parsedValue.asMilliseconds() / dateHistogramInterval)) + parsedValue === 'previous' || + parsedValue === 'invalid' || + Number.isInteger(parsedValue.asMilliseconds() / dateHistogramInterval) ); }) .map(({ value }) => value); diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/last_value.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/last_value.tsx index db9c7c0d6ab0c..cde3f10b64c0b 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/last_value.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/last_value.tsx @@ -29,7 +29,6 @@ import { getFilter, } from './helpers'; import { adjustTimeScaleLabelSuffix } from '../time_scale_utils'; -import { getDisallowedPreviousShiftMessage } from '../../time_shift_utils'; import { isRuntimeField, isScriptedField } from './terms/helpers'; import { FormRow } from './shared_components/form_row'; import { getColumnReducedTimeRangeError } from '../../reduced_time_range_utils'; @@ -214,7 +213,6 @@ export const lastValueOperation: OperationDefinition< if (invalidSortFieldMessage) { errorMessages = [invalidSortFieldMessage]; } - errorMessages.push(...(getDisallowedPreviousShiftMessage(layer, columnId) || [])); errorMessages.push(...(getColumnReducedTimeRangeError(layer, columnId, indexPattern) || [])); return errorMessages.length ? errorMessages : undefined; }, diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/metrics.tsx index bb15831a31854..4c11393af08e0 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/metrics.tsx @@ -25,7 +25,6 @@ import { ValueFormatConfig, } from './column_types'; import { adjustTimeScaleLabelSuffix } from '../time_scale_utils'; -import { getDisallowedPreviousShiftMessage } from '../../time_shift_utils'; import { updateColumnParam } from '../layer_helpers'; import { getColumnReducedTimeRangeError } from '../../reduced_time_range_utils'; import { getGroupByKey } from './get_group_by_key'; @@ -215,7 +214,6 @@ function buildMetricOperation>({ layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern ), - getDisallowedPreviousShiftMessage(layer, columnId), getColumnReducedTimeRangeError(layer, columnId, indexPattern), ]), filterable: true, diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/percentile.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/percentile.tsx index b55b1325a245e..34f9f854d3745 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/percentile.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/percentile.tsx @@ -28,7 +28,6 @@ import { import { FieldBasedIndexPatternColumn } from './column_types'; import { adjustTimeScaleLabelSuffix } from '../time_scale_utils'; import { useDebouncedValue } from '../../../../shared_components'; -import { getDisallowedPreviousShiftMessage } from '../../time_shift_utils'; import { FormRow } from './shared_components'; import { getColumnReducedTimeRangeError } from '../../reduced_time_range_utils'; import { getGroupByKey, groupByKey } from './get_group_by_key'; @@ -290,7 +289,6 @@ export const percentileOperation: OperationDefinition< getErrorMessage: (layer, columnId, indexPattern) => combineErrorMessages([ getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), - getDisallowedPreviousShiftMessage(layer, columnId), getColumnReducedTimeRangeError(layer, columnId, indexPattern), ]), paramEditor: function PercentileParamEditor({ diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/percentile_ranks.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/percentile_ranks.tsx index 3d7b12802858c..3aadcb293114e 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/percentile_ranks.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/percentile_ranks.tsx @@ -23,7 +23,6 @@ import { import { FieldBasedIndexPatternColumn } from './column_types'; import { adjustTimeScaleLabelSuffix } from '../time_scale_utils'; import { useDebouncedValue } from '../../../../shared_components'; -import { getDisallowedPreviousShiftMessage } from '../../time_shift_utils'; import { FormRow } from './shared_components'; import { getColumnReducedTimeRangeError } from '../../reduced_time_range_utils'; @@ -168,7 +167,6 @@ export const percentileRanksOperation: OperationDefinition< getErrorMessage: (layer, columnId, indexPattern) => combineErrorMessages([ getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), - getDisallowedPreviousShiftMessage(layer, columnId), getColumnReducedTimeRangeError(layer, columnId, indexPattern), ]), paramEditor: function PercentileParamEditor({ diff --git a/x-pack/plugins/lens/public/datasources/form_based/time_shift_utils.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/time_shift_utils.test.tsx index 3405752d3ec4f..a2495fbbbc1f6 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/time_shift_utils.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/time_shift_utils.test.tsx @@ -6,80 +6,9 @@ */ import moment from 'moment'; -import { getDisallowedPreviousShiftMessage, resolveTimeShift } from './time_shift_utils'; -import { FormBasedLayer } from './types'; +import { resolveTimeShift } from './time_shift_utils'; describe('time_shift_utils', () => { - describe('getDisallowedPreviousShiftMessage', () => { - const layer: FormBasedLayer = { - indexPatternId: '', - columnOrder: [], - columns: { - a: { - operationType: 'date_histogram', - dataType: 'date', - isBucketed: true, - label: '', - references: [], - sourceField: 'timestamp', - }, - b: { - operationType: 'count', - dataType: 'number', - isBucketed: false, - label: 'non shifted', - references: [], - sourceField: 'records', - }, - c: { - operationType: 'count', - dataType: 'number', - isBucketed: false, - label: 'shifted', - timeShift: '1d', - references: [], - sourceField: 'records', - }, - }, - }; - - it('shoud not produce an error for no shift', () => { - expect(getDisallowedPreviousShiftMessage(layer, 'b')).toBeUndefined(); - }); - - it('shoud not produce an error for non-previous shift', () => { - expect(getDisallowedPreviousShiftMessage(layer, 'c')).toBeUndefined(); - }); - - it('shoud produce an error for previous shift with date histogram', () => { - expect( - getDisallowedPreviousShiftMessage( - { - ...layer, - columns: { ...layer.columns, c: { ...layer.columns.c, timeShift: 'previous' } }, - }, - 'c' - ) - ).toHaveLength(1); - }); - - it('shoud not produce an error for previous shift without date histogram', () => { - expect( - getDisallowedPreviousShiftMessage( - { - ...layer, - columns: { - ...layer.columns, - a: { ...layer.columns.a, operationType: 'terms' }, - c: { ...layer.columns.c, timeShift: 'previous' }, - }, - }, - 'c' - ) - ).toBeUndefined(); - }); - }); - describe('resolveTimeShift', () => { const dateString = '2022-11-02T00:00:00.000Z'; // shift by 2 days + 2500 s (to get a shift which is not a multiple of the given interval) @@ -113,6 +42,10 @@ describe('time_shift_utils', () => { .toBe('261000s'); }); + it('should convert previous relative time shift to seconds (rounded) when a date histogram is present', () => { + expect(resolveTimeShift(`previous`, getDateRange(), 100, true)).toBe('171000s'); + }); + it('should always include the passed date in the computed interval', () => { const dateRange = getDateRange(); for (const anchor of ['startAt', 'endAt']) { diff --git a/x-pack/plugins/lens/public/datasources/form_based/time_shift_utils.tsx b/x-pack/plugins/lens/public/datasources/form_based/time_shift_utils.tsx index b673f00a34392..c287a93675f64 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/time_shift_utils.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/time_shift_utils.tsx @@ -23,10 +23,13 @@ import type { FormBasedLayer, FormBasedPrivateState } from './types'; import type { FramePublicAPI, IndexPattern } from '../../types'; export function parseTimeShiftWrapper(timeShiftString: string, dateRange: DateRange) { - return isAbsoluteTimeShift(timeShiftString.trim()) - ? parseAbsoluteTimeShift(timeShiftString, { from: dateRange.fromDate, to: dateRange.toDate }) - .value - : parseTimeShift(timeShiftString); + if (isAbsoluteTimeShift(timeShiftString.trim())) { + return parseAbsoluteTimeShift(timeShiftString, { + from: dateRange.fromDate, + to: dateRange.toDate, + }).value; + } + return parseTimeShift(timeShiftString); } export const timeShiftOptions = [ @@ -142,7 +145,6 @@ export function getDateHistogramInterval( export function getLayerTimeShiftChecks({ interval: dateHistogramInterval, - hasDateHistogram, canShift, }: ReturnType) { return { @@ -164,42 +166,11 @@ export function getLayerTimeShiftChecks({ ); }, isInvalid: (parsedValue: ReturnType) => { - return Boolean( - parsedValue === 'invalid' || (hasDateHistogram && parsedValue && parsedValue === 'previous') - ); + return Boolean(parsedValue === 'invalid'); }, }; } -export function getDisallowedPreviousShiftMessage( - layer: FormBasedLayer, - columnId: string -): string[] | undefined { - const currentColumn = layer.columns[columnId]; - const hasPreviousShift = - currentColumn.timeShift && - !isAbsoluteTimeShift(currentColumn.timeShift) && - parseTimeShift(currentColumn.timeShift) === 'previous'; - if (!hasPreviousShift) { - return; - } - const hasDateHistogram = Object.values(layer.columns).some( - (column) => column.operationType === 'date_histogram' - ); - if (!hasDateHistogram) { - return; - } - return [ - i18n.translate('xpack.lens.indexPattern.dateHistogramTimeShift', { - defaultMessage: - 'In a single layer, you are unable to combine previous time range shift with date histograms. Either use an explicit time shift duration in "{column}" or replace the date histogram.', - values: { - column: currentColumn.label, - }, - }), - ]; -} - export function getStateTimeShiftWarningMessages( datatableUtilities: DatatableUtilitiesService, state: FormBasedPrivateState, @@ -346,10 +317,15 @@ function roundAbsoluteInterval(timeShift: string, dateRange: DateRange, targetBa export function resolveTimeShift( timeShift: string | undefined, dateRange: DateRange, - targetBars: number + targetBars: number, + hasDateHistogram: boolean = false ) { if (timeShift && isAbsoluteTimeShift(timeShift)) { return roundAbsoluteInterval(timeShift, dateRange, targetBars); } + // Translate a relative "previous" shift into an absolute endAt() + if (timeShift && hasDateHistogram && timeShift === 'previous') { + return roundAbsoluteInterval(`endAt(${dateRange.fromDate})`, dateRange, targetBars); + } return timeShift; } diff --git a/x-pack/plugins/lens/public/datasources/form_based/to_expression.ts b/x-pack/plugins/lens/public/datasources/form_based/to_expression.ts index 61e17c72ca829..9ec654c47868f 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/to_expression.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/to_expression.ts @@ -158,7 +158,12 @@ function getExpressionForLayer( let aggAst = def.toEsAggsFn( { ...col, - timeShift: resolveTimeShift(col.timeShift, dateRange, histogramBarsTarget), + timeShift: resolveTimeShift( + col.timeShift, + dateRange, + histogramBarsTarget, + hasDateHistogram + ), }, wrapInFilter || wrapInTimeFilter ? `${aggId}-metric` : aggId, indexPattern, @@ -181,11 +186,21 @@ function getExpressionForLayer( schema: 'bucket', filter: col.filter && queryToAst(col.filter), timeWindow: wrapInTimeFilter ? col.reducedTimeRange : undefined, - timeShift: resolveTimeShift(col.timeShift, dateRange, histogramBarsTarget), + timeShift: resolveTimeShift( + col.timeShift, + dateRange, + histogramBarsTarget, + hasDateHistogram + ), }), ]), customMetric: buildExpression({ type: 'expression', chain: [aggAst] }), - timeShift: resolveTimeShift(col.timeShift, dateRange, histogramBarsTarget), + timeShift: resolveTimeShift( + col.timeShift, + dateRange, + histogramBarsTarget, + hasDateHistogram + ), } ).toAst(); } diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/index.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/index.ts index c7232fefb6efd..15dd3c2b87247 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/index.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/index.ts @@ -22,3 +22,4 @@ export * from './test_now_mode.journey'; export * from './data_retention.journey'; export * from './monitor_details_page/monitor_summary.journey'; export * from './test_run_details.journey'; +export * from './step_details.journey'; diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/step_details.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/step_details.journey.ts new file mode 100644 index 0000000000000..94172d2c2bde6 --- /dev/null +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/step_details.journey.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { journey, step, before, after } from '@elastic/synthetics'; +import { recordVideo } from '@kbn/observability-plugin/e2e/record_video'; +import { syntheticsAppPageProvider } from '../../page_objects/synthetics/synthetics_app'; +import { SyntheticsServices } from './services/synthetics_services'; + +journey(`StepDetailsPage`, async ({ page, params }) => { + recordVideo(page); + + page.setDefaultTimeout(60 * 1000); + const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl }); + + const services = new SyntheticsServices(params); + + before(async () => { + await services.cleaUp(); + await services.enableMonitorManagedViaApi(); + await services.addTestMonitor( + 'https://www.google.com', + { + type: 'browser', + urls: 'https://www.google.com', + custom_heartbeat_id: 'a47bfc4e-361a-4eb0-83f3-b5bb68781b5b', + locations: [ + { id: 'us_central', label: 'North America - US Central', isServiceManaged: true }, + ], + }, + 'a47bfc4e-361a-4eb0-83f3-b5bb68781b5b' + ); + }); + + after(async () => { + await services.cleaUp(); + }); + + step('Go to step details page', async () => { + await syntheticsApp.navigateToStepDetails({ + stepIndex: 1, + checkGroup: 'ab240846-8d22-11ed-8fac-52bb19a2321e', + configId: 'a47bfc4e-361a-4eb0-83f3-b5bb68781b5b', + }); + }); + + step('it shows metrics', async () => { + await page.waitForSelector('text=558 KB'); + await page.waitForSelector('text=402 ms'); + await page.waitForSelector('text=521 ms'); + }); +}); diff --git a/x-pack/plugins/synthetics/e2e/page_objects/synthetics/synthetics_app.tsx b/x-pack/plugins/synthetics/e2e/page_objects/synthetics/synthetics_app.tsx index 1732c3f0e91b8..f9e57b952961c 100644 --- a/x-pack/plugins/synthetics/e2e/page_objects/synthetics/synthetics_app.tsx +++ b/x-pack/plugins/synthetics/e2e/page_objects/synthetics/synthetics_app.tsx @@ -50,6 +50,24 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib } }, + async navigateToStepDetails({ + configId, + stepIndex, + checkGroup, + doLogin = true, + }: { + checkGroup: string; + configId: string; + stepIndex: number; + doLogin?: boolean; + }) { + const stepDetails = `/monitor/${configId}/test-run/${checkGroup}/step/${stepIndex}?locationId=us_central`; + await page.goto(overview + stepDetails, { waitUntil: 'networkidle' }); + if (doLogin) { + await this.loginToKibana(); + } + }, + async waitForMonitorManagementLoadingToFinish() { while (true) { if ((await page.$(this.byTestId('uptimeLoader'))) === null) break; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/result_details.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/result_details.tsx index 54f0fe8412551..dc5a2b2f3f62c 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/result_details.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/result_details.tsx @@ -54,10 +54,10 @@ export const TimingDetails = ({ step }: { step: JourneyStep }) => { ) : ( ), }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/common/network_data/data_formatting.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/common/network_data/data_formatting.ts index fb12506065cde..aa1a64d9eb447 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/common/network_data/data_formatting.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/common/network_data/data_formatting.ts @@ -491,8 +491,11 @@ export const colourPalette: ColourPalette = { ...TIMING_PALETTE, ...MIME_TYPE_PA export const formatTooltipHeading = (index: number, fullText: string): string => isNaN(index) ? fullText : `${index}. ${fullText}`; -export const formatMillisecond = (ms: number, digits?: number) => { - if (ms < 1000) { +export const formatMillisecond = ( + ms: number, + { maxMillis = 1000, digits }: { digits?: number; maxMillis?: number } +) => { + if (ms < maxMillis) { return `${ms.toFixed(digits ?? 0)} ms`; } return `${(ms / 1000).toFixed(digits ?? 1)} s`; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_object_metrics.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_object_metrics.ts index fd19ec7675ec6..6cc622c8e19b5 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_object_metrics.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_object_metrics.ts @@ -7,31 +7,59 @@ import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; +import { usePreviousObjectMetrics } from './use_prev_object_metrics'; import { MIME_FILTERS, MimeType, MimeTypesMap } from '../common/network_data/types'; import { networkEventsSelector } from '../../../state/network_events/selectors'; export const useObjectMetrics = () => { const { checkGroupId, stepIndex } = useParams<{ checkGroupId: string; stepIndex: string }>(); + const { mimeData: prevMimeData } = usePreviousObjectMetrics(); + const _networkEvents = useSelector(networkEventsSelector); const networkEvents = _networkEvents[checkGroupId ?? '']?.[Number(stepIndex)]; - const objectTypeCounts: Record = {}; - const objectTypeWeights: Record = {}; + const objectTypeCounts: Record = {}; + const objectTypeWeights: Record = {}; networkEvents?.events.forEach((event) => { if (event.mimeType) { - objectTypeCounts[MimeTypesMap[event.mimeType] ?? MimeType.Other] = - (objectTypeCounts[MimeTypesMap[event.mimeType] ?? MimeType.Other] ?? 0) + 1; - objectTypeWeights[MimeTypesMap[event.mimeType] ?? MimeType.Other] = - (objectTypeWeights[MimeTypesMap[event.mimeType] ?? MimeType.Other] ?? 0) + - (event.transferSize || 0); + const mimeType = MimeTypesMap[event.mimeType] ?? MimeType.Other; + + if (objectTypeCounts[mimeType]) { + objectTypeCounts[mimeType].value++; + } else { + objectTypeCounts[mimeType] = { value: 1, prevValue: 0 }; + } + + if (objectTypeWeights[mimeType]) { + objectTypeWeights[mimeType].value += event.transferSize || 0; + } else { + objectTypeWeights[mimeType] = { + value: event.transferSize || 0, + prevValue: 0, + }; + } } }); - const totalObjects = Object.values(objectTypeCounts).reduce((acc, val) => acc + val, 0); + const totalObjects = Object.values(objectTypeCounts).reduce((acc, val) => acc + val.value, 0); - const totalObjectsWeight = Object.values(objectTypeWeights).reduce((acc, val) => acc + val, 0); + const totalObjectsWeight = Object.values(objectTypeWeights).reduce( + (acc, val) => acc + val.value, + 0 + ); + + Object.keys(prevMimeData).forEach((mimeType) => { + const mimeTypeKey = MimeTypesMap[mimeType] ?? MimeType.Other; + if (objectTypeCounts[mimeTypeKey]) { + objectTypeCounts[mimeTypeKey].prevValue += prevMimeData[mimeType].count; + } + + if (objectTypeWeights[mimeTypeKey]) { + objectTypeWeights[mimeTypeKey].prevValue += prevMimeData[mimeType].weight; + } + }); return { loading: networkEvents?.loading ?? true, @@ -39,16 +67,41 @@ export const useObjectMetrics = () => { totalObjectsWeight: formatBytes(totalObjectsWeight), items: MIME_FILTERS.map(({ label, mimeType }) => ({ label, - count: objectTypeCounts[mimeType] ?? 0, - total: totalObjects, mimeType, - percent: ((objectTypeCounts[mimeType] ?? 0) / totalObjects) * 100, - weight: formatBytes(objectTypeWeights[mimeType] ?? 0), - weightPercent: ((objectTypeWeights[mimeType] ?? 0) / totalObjectsWeight) * 100, + total: totalObjects, + count: objectTypeCounts?.[mimeType]?.value ?? 0, + percent: ((objectTypeCounts?.[mimeType]?.value ?? 0) / totalObjects) * 100, + weight: formatBytes(objectTypeWeights[mimeType]?.value ?? 0), + weightPercent: ((objectTypeWeights[mimeType]?.value ?? 0) / totalObjectsWeight) * 100, + + countDelta: getDeltaPercent( + objectTypeCounts?.[mimeType]?.value ?? 0, + objectTypeCounts?.[mimeType]?.prevValue ?? 0 + ), + weightDelta: getWeightDeltaPercent( + objectTypeWeights?.[mimeType]?.value, + objectTypeWeights?.[mimeType]?.prevValue + ), })), }; }; +export const getWeightDeltaPercent = (current: number, previous: number) => { + if (previous === 0 || !previous) { + return 0; + } + + return (((current - previous) / previous) * 100).toFixed(0); +}; + +export const getDeltaPercent = (current: number, previous: number) => { + if (previous === 0) { + return 0; + } + + return (((current - previous) / previous) * 100).toFixed(0); +}; + export const formatBytes = (bytes: number, decimals = 0) => { if (bytes === 0) return '0 Bytes'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_prev_object_metrics.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_prev_object_metrics.ts new file mode 100644 index 0000000000000..1a5d31415decb --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/hooks/use_prev_object_metrics.ts @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useParams } from 'react-router-dom'; +import moment from 'moment'; +import { useJourneySteps } from '../../monitor_details/hooks/use_journey_steps'; +import { useReduxEsSearch } from '../../../hooks/use_redux_es_search'; +import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants'; + +export const MONITOR_DURATION_US = 'monitor.duration.us'; +export const SYNTHETICS_CLS = 'browser.experience.cls'; +export const SYNTHETICS_LCP = 'browser.experience.lcp.us'; +export const SYNTHETICS_FCP = 'browser.experience.fcp.us'; +export const SYNTHETICS_ONLOAD_EVENT = 'browser.experience.load.us'; +export const SYNTHETICS_DCL = 'browser.experience.dcl.us'; +export const SYNTHETICS_STEP_NAME = 'synthetics.step.name.keyword'; +export const SYNTHETICS_STEP_DURATION = 'synthetics.step.duration.us'; + +export type PreviousObjectMetrics = ReturnType; + +export const usePreviousObjectMetrics = () => { + const { monitorId, stepIndex, checkGroupId } = useParams<{ + monitorId: string; + stepIndex: string; + checkGroupId: string; + }>(); + + const { data } = useJourneySteps(); + + const timestamp = data?.details?.timestamp; + + const { data: prevObjectMetrics } = useReduxEsSearch( + { + index: SYNTHETICS_INDEX_PATTERN, + body: { + track_total_hits: false, + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + size: 0, + runtime_mappings: { + 'synthetics.payload.transfer_size': { + type: 'long', + }, + }, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + lte: timestamp ?? 'now', + gte: moment(timestamp).subtract(1, 'day').toISOString(), + }, + }, + }, + { + term: { + config_id: monitorId, + }, + }, + { + term: { + 'synthetics.type': 'journey/network_info', + }, + }, + { + term: { + 'synthetics.step.index': stepIndex, + }, + }, + { + range: { + '@timestamp': { + gte: 'now-24h/h', + lte: 'now', + }, + }, + }, + ], + must_not: [ + { + term: { + 'monitor.check_group': { + value: checkGroupId, + }, + }, + }, + ], + }, + }, + aggs: { + testRuns: { + cardinality: { + field: 'monitor.check_group', + }, + }, + objectCounts: { + terms: { + field: 'http.response.mime_type', + size: 500, + }, + aggs: { + weight: { + sum: { + field: 'synthetics.payload.transfer_size', + }, + }, + }, + }, + }, + }, + }, + [stepIndex, monitorId, checkGroupId], + { + name: `previousObjectMetrics/${monitorId}/${checkGroupId}/${stepIndex}/`, + isRequestReady: !!timestamp, + } + ); + + const mimeData: Record = {}; + + const testRuns = prevObjectMetrics?.aggregations?.testRuns?.value ?? 0; + + prevObjectMetrics?.aggregations?.objectCounts?.buckets?.forEach((bucket) => { + mimeData[bucket.key] = { + weight: bucket.weight.value ? bucket.weight.value / testRuns : 0, + count: bucket.doc_count / testRuns, + }; + }); + + return { mimeData }; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/network_timings_breakdown.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/network_timings_breakdown.tsx index 27ec3e6deffbb..cd9a89c7e5391 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/network_timings_breakdown.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/network_timings_breakdown.tsx @@ -11,6 +11,8 @@ import { ReportTypes } from '@kbn/observability-plugin/public'; import { EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ClientPluginsStart } from '../../../../plugin'; +import { useSelectedLocation } from '../monitor_details/hooks/use_selected_location'; +import { LoadingState } from '../monitors_page/overview/overview/monitor_detail_flyout'; export const NetworkTimingsBreakdown = ({ monitorId }: { monitorId: string }) => { const { observability } = useKibana().services; @@ -19,6 +21,11 @@ export const NetworkTimingsBreakdown = ({ monitorId }: { monitorId: string }) => const { stepIndex } = useParams<{ checkGroupId: string; stepIndex: string }>(); + const selectedLocation = useSelectedLocation(); + if (!selectedLocation) { + return ; + } + return ( <> @@ -47,6 +54,10 @@ export const NetworkTimingsBreakdown = ({ monitorId }: { monitorId: string }) => field: 'synthetics.step.index', values: [stepIndex], }, + { + field: 'observer.geo.name', + values: [selectedLocation.label], + }, ], }, ]} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_detail_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_detail_page.tsx index dfb72e3315cbd..e190a45bd38ba 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_detail_page.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_detail_page.tsx @@ -89,7 +89,7 @@ export const StepDetailPage = () => { - + diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_metrics/step_metrics.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_metrics/step_metrics.tsx index 3845fdf2def59..1539c98111c5f 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_metrics/step_metrics.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_metrics/step_metrics.tsx @@ -134,7 +134,7 @@ const StatThreshold = ({ {title} {isSame ? ( - + ) : ( )} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_objects/color_palette.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_objects/color_palette.tsx index 7d43f7d1dd26f..78fbc568a0983 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_objects/color_palette.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_objects/color_palette.tsx @@ -6,9 +6,17 @@ */ import React, { useState, useEffect } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiLoadingContent } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiLoadingContent, + EuiIcon, + EuiToolTip, +} from '@elastic/eui'; import { useTheme } from '@kbn/observability-plugin/public'; import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; import { colourPalette } from '../common/network_data/data_formatting'; export const ColorPalette = ({ @@ -17,17 +25,52 @@ export const ColorPalette = ({ percent, value, loading, + delta, + hasAnyThresholdBreach, labelWidth = 40, - valueWidth = 60, + valueWidth = 65, }: { label: string; mimeType: string; percent: number; + delta: number; value: string; loading: boolean; + hasAnyThresholdBreach: boolean; labelWidth?: number; valueWidth?: number; }) => { + const getToolTipContent = () => { + return i18n.translate('xpack.synthetics.stepDetails.palette.tooltip.label', { + defaultMessage: 'Value is {deltaLabel} compared to steps in previous 24 hours.', + values: { + deltaLabel: + Math.abs(delta) === 0 + ? i18n.translate('xpack.synthetics.stepDetails.palette.tooltip.noChange', { + defaultMessage: 'same', + }) + : delta > 0 + ? i18n.translate('xpack.synthetics.stepDetails.palette.increased', { + defaultMessage: '{delta}% higher', + values: { delta }, + }) + : i18n.translate('xpack.synthetics.stepDetails.palette.decreased', { + defaultMessage: '{delta}% lower', + values: { delta }, + }), + }, + }); + }; + + const getColor = () => { + if (Math.abs(delta) < 5) { + return 'default'; + } + return delta > 5 ? 'danger' : 'success'; + }; + + const hasDelta = Math.abs(delta) > 0; + return ( @@ -45,10 +88,22 @@ export const ColorPalette = ({ size="s" style={{ fontWeight: 'bold', whiteSpace: 'nowrap' }} className="eui-textRight" + color={getColor()} > {value} + {hasAnyThresholdBreach && ( + + + {hasDelta ? ( + 0 ? 'sortUp' : 'sortDown'} size="m" color={getColor()} /> + ) : ( + + )} + + + )} ); }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_objects/object_count_list.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_objects/object_count_list.tsx index bbfec1d56fc2f..ec4555da24dc0 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_objects/object_count_list.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_objects/object_count_list.tsx @@ -14,6 +14,10 @@ import { useObjectMetrics } from '../hooks/use_object_metrics'; export const ObjectCountList = () => { const objectMetrics = useObjectMetrics(); + const hasAnyThresholdBreach = objectMetrics.items.some( + ({ countDelta }) => Math.abs(Number(countDelta)) > 5 + ); + return ( <> @@ -28,17 +32,19 @@ export const ObjectCountList = () => { - +
- {objectMetrics.items.map(({ label, mimeType, percent, count }) => ( + {objectMetrics.items.map(({ label, mimeType, percent, count, countDelta }) => ( diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_objects/object_weight_list.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_objects/object_weight_list.tsx index c787fcc0711ba..186f92b9782df 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_objects/object_weight_list.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_objects/object_weight_list.tsx @@ -13,7 +13,9 @@ import { useObjectMetrics } from '../hooks/use_object_metrics'; export const ObjectWeightList = () => { const objectMetrics = useObjectMetrics(); - + const hasAnyThresholdBreach = objectMetrics.items.some( + ({ weightDelta }) => Math.abs(Number(weightDelta)) > 5 + ); return ( <> @@ -29,16 +31,18 @@ export const ObjectWeightList = () => { - +
- {objectMetrics.items.map(({ label, mimeType, weightPercent, weight }) => ( + {objectMetrics.items.map(({ label, mimeType, weightPercent, weight, weightDelta }) => ( {' '} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_timing_breakdown/breakdown_legend.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_timing_breakdown/breakdown_legend.tsx index 46c81f98198bc..6e2e0a144755c 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_timing_breakdown/breakdown_legend.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_timing_breakdown/breakdown_legend.tsx @@ -32,7 +32,7 @@ export const BreakdownLegend = () => { - {formatMillisecond(value)} + {formatMillisecond(value, {})} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_timing_breakdown/network_timings_donut.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_timing_breakdown/network_timings_donut.tsx index 439cc300afe4f..075ae8a4ce84d 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_timing_breakdown/network_timings_donut.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_timing_breakdown/network_timings_donut.tsx @@ -51,14 +51,14 @@ export const NetworkTimingsDonut = () => {

{TIMINGS_BREAKDOWN}

- + d?.value} - valueFormatter={(d: number) => formatMillisecond(d)} + valueFormatter={(d: number) => formatMillisecond(d, {})} layers={[ { groupByRollup: (d: Datum) => d.label, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_waterfall_chart/waterfall/waterfall_marker/waterfall_markers.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_waterfall_chart/waterfall/waterfall_marker/waterfall_markers.tsx index e2337746cbcd0..c1a8db93c3e6a 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_waterfall_chart/waterfall/waterfall_marker/waterfall_markers.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_waterfall_chart/waterfall/waterfall_marker/waterfall_markers.tsx @@ -13,6 +13,7 @@ import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { MarkerItems, useWaterfallContext } from '../context/waterfall_context'; import { WaterfallMarkerIcon } from './waterfall_marker_icon'; +import { formatMillisecond } from '../../../common/network_data/data_formatting'; export const FIELD_SYNTHETICS_LCP = 'browser.experience.lcp.us'; export const FIELD_SYNTHETICS_FCP = 'browser.experience.fcp.us'; @@ -98,10 +99,7 @@ export function WaterfallChartMarkers() { { dataValue: offset, details: label, - header: i18n.translate('xpack.synthetics.synthetics.waterfall.offsetUnit', { - defaultMessage: '{offset} ms', - values: { offset }, - }), + header: formatMillisecond(offset, { maxMillis: 4000 }), }, ]} marker={} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_redux_es_search.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_redux_es_search.ts index 5afb38431878d..01130c61faad2 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_redux_es_search.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_redux_es_search.ts @@ -23,9 +23,10 @@ export const useReduxEsSearch = < >( params: TParams, fnDeps: any[], - options: { inspector?: IInspectorInfo; name: string } + options: { inspector?: IInspectorInfo; name: string; isRequestReady?: boolean } ) => { - const { name } = options ?? {}; + const { name, isRequestReady = true } = options ?? {}; + const dispatch = useDispatch(); const loadings = useSelector(selectEsQueryLoading); @@ -33,11 +34,11 @@ export const useReduxEsSearch = < const errors = useSelector(selectEsQueryError); useEffect(() => { - if (params.index) { + if (params.index && isRequestReady) { dispatch(executeEsQueryAction.get({ params, name })); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dispatch, name, JSON.stringify(params)]); + }, [dispatch, name, JSON.stringify(params), isRequestReady]); return useMemo(() => { return { diff --git a/x-pack/plugins/synthetics/server/queries/get_journey_details.ts b/x-pack/plugins/synthetics/server/queries/get_journey_details.ts index bacc3cf64ae48..f5b67f8e0e6a4 100644 --- a/x-pack/plugins/synthetics/server/queries/get_journey_details.ts +++ b/x-pack/plugins/synthetics/server/queries/get_journey_details.ts @@ -65,6 +65,11 @@ export const getJourneyDetails: UMElasticsearchQueryFn< 'monitor.id': thisJourneySource.monitor.id, }, }, + { + term: { + 'observer.geo.name': thisJourneySource.observer?.geo?.name, + }, + }, { term: { 'synthetics.type': 'journey/start', diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 333af20736bac..fd10f3281b25b 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -10135,12 +10135,10 @@ "xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundForNameTitle": " pour \"{name}\"", "xpack.csp.benchmarks.totalIntegrationsCountMessage": "Affichage de {pageCount} sur {totalCount, plural, one {# intégration} other {# intégrations}}", "xpack.csp.cloudPosturePage.errorRenderer.errorDescription": "{error} {statusCode} : {body}", - "xpack.csp.complianceDashboard.complianceByCisSection.complianceColumnTooltip": "{passed}/{total}", "xpack.csp.dashboard.benchmarkSection.clusterTitle": "{title} - {shortId}", "xpack.csp.dashboard.benchmarkSection.clusterTitleTooltip.clusterTitle": "{title} - {shortId}", "xpack.csp.dashboard.benchmarkSection.lastEvaluatedTitle": "Dernière évaluation {dateFromNow}", "xpack.csp.findings.distributionBar.showingPageOfTotalLabel": "Affichage de {pageStart}-{pageEnd} sur {total} {type}", - "xpack.csp.findings.findingsByResourceTable.failedFindingsToolTip": "{failed} sur {total}", "xpack.csp.findings.findingsTableCell.addFilterButton": "Ajouter un filtre {field}", "xpack.csp.findings.findingsTableCell.addNegateFilterButton": "Ajouter un filtre {field} négatif", "xpack.csp.findings.resourceFindings.resourceFindingsPageTitle": "{resourceName} - Résultats", @@ -10197,7 +10195,6 @@ "xpack.csp.findings.findingsByResource.noFindingsTitle": "Il n'y a aucun résultat", "xpack.csp.findings.findingsByResource.tableRowTypeLabel": "Ressources", "xpack.csp.findings.findingsByResourceTable.cisSectionsColumnLabel": "Sections CIS", - "xpack.csp.findings.findingsByResourceTable.failedFindingsColumnLabel": "Échec des résultats", "xpack.csp.findings.findingsErrorToast.searchFailedTitle": "Échec de la recherche", "xpack.csp.findings.findingsFlyout.jsonTabTitle": "JSON", "xpack.csp.findings.findingsFlyout.overviewTab.actualTitle": "Réel", @@ -10239,7 +10236,6 @@ "xpack.csp.findings.findingsTable.findingsTableColumn.resourceTypeColumnLabel": "Type de ressource", "xpack.csp.findings.findingsTable.findingsTableColumn.resultColumnLabel": "Résultat", "xpack.csp.findings.findingsTable.findingsTableColumn.ruleBenchmarkColumnLabel": "Benchmark", - "xpack.csp.findings.findingsTable.findingsTableColumn.ruleColumnLabel": "Règle", "xpack.csp.findings.findingsTable.findingsTableColumn.ruleSectionColumnLabel": "Section CIS", "xpack.csp.findings.groupBySelector.groupByLabel": "Regrouper par", "xpack.csp.findings.groupBySelector.groupByNoneLabel": "Aucun", @@ -18115,7 +18111,6 @@ "xpack.lens.indexPattern.cumulativeSumOf": "Somme cumulée de {name}", "xpack.lens.indexPattern.dateHistogram.autoLongerExplanation": "Pour choisir l'intervalle, Lens divise la plage temporelle spécifiée par le paramètre avancé {targetBarSetting} et calcule le meilleur intervalle pour vos données. Par exemple, lorsque la plage temporelle est de 4 jours, les données sont divisées en compartiments horaires. Pour configurer le nombre de barres maximal, utilisez le paramètre avancé {maxBarSetting}.", "xpack.lens.indexPattern.dateHistogram.restrictedInterval": "Intervalle fixé à {intervalValue} en raison de restrictions d'agrégation.", - "xpack.lens.indexPattern.dateHistogramTimeShift": "Dans un calque unique, vous ne pouvez pas combiner un décalage de plage temporelle précédent avec des histogrammes de dates. Utilisez une durée de décalage temporel explicite dans \"{column}\" ou remplacez l’histogramme de dates.", "xpack.lens.indexPattern.derivativeOf": "Différences de {name}", "xpack.lens.indexPattern.fieldNoOperation": "Le champ {field} ne peut pas être utilisé sans opération", "xpack.lens.indexPattern.fieldsNotFound": "{count, plural, one {Champ} other {Champs}} {missingFields} {count, plural, one {introuvable} other {introuvables}}", @@ -36205,7 +36200,6 @@ "cases.components.status.inProgress": "En cours", "cases.components.status.open": "Ouvrir", "devTools.badge.betaLabel": "Bêta", - "devTools.badge.betaTooltipText": "Cette fonctionnalité pourra considérablement changer dans les futures versions", "devTools.badge.readOnly.text": "Lecture seule", "devTools.badge.readOnly.tooltip": "Enregistrement impossible", "devTools.breadcrumb.homeLabel": "Outils de développement", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8d1b7e69b80e1..f7fe9e893d3d6 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10124,12 +10124,10 @@ "xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundForNameTitle": " \"{name}\"", "xpack.csp.benchmarks.totalIntegrationsCountMessage": "{pageCount}/{totalCount, plural, other {#個の統合}}を表示しています", "xpack.csp.cloudPosturePage.errorRenderer.errorDescription": "{error} {statusCode}: {body}", - "xpack.csp.complianceDashboard.complianceByCisSection.complianceColumnTooltip": "{passed}/{total}", "xpack.csp.dashboard.benchmarkSection.clusterTitle": "{title} - {shortId}", "xpack.csp.dashboard.benchmarkSection.clusterTitleTooltip.clusterTitle": "{title} - {shortId}", "xpack.csp.dashboard.benchmarkSection.lastEvaluatedTitle": "前回の評価{dateFromNow}", "xpack.csp.findings.distributionBar.showingPageOfTotalLabel": "{total}件中{pageStart}-{pageEnd}件の{type}を表示しています", - "xpack.csp.findings.findingsByResourceTable.failedFindingsToolTip": "{total}件中{failed}件", "xpack.csp.findings.findingsTableCell.addFilterButton": "{field}フィルターを追加", "xpack.csp.findings.findingsTableCell.addNegateFilterButton": "{field}否定フィルターを追加", "xpack.csp.findings.resourceFindings.resourceFindingsPageTitle": "{resourceName} - 調査結果", @@ -10186,7 +10184,6 @@ "xpack.csp.findings.findingsByResource.noFindingsTitle": "調査結果はありません", "xpack.csp.findings.findingsByResource.tableRowTypeLabel": "リソース", "xpack.csp.findings.findingsByResourceTable.cisSectionsColumnLabel": "CISセクション", - "xpack.csp.findings.findingsByResourceTable.failedFindingsColumnLabel": "失敗した調査結果", "xpack.csp.findings.findingsErrorToast.searchFailedTitle": "検索失敗", "xpack.csp.findings.findingsFlyout.jsonTabTitle": "JSON", "xpack.csp.findings.findingsFlyout.overviewTab.actualTitle": "実際", @@ -10228,7 +10225,6 @@ "xpack.csp.findings.findingsTable.findingsTableColumn.resourceTypeColumnLabel": "リソースタイプ", "xpack.csp.findings.findingsTable.findingsTableColumn.resultColumnLabel": "結果", "xpack.csp.findings.findingsTable.findingsTableColumn.ruleBenchmarkColumnLabel": "ベンチマーク", - "xpack.csp.findings.findingsTable.findingsTableColumn.ruleColumnLabel": "ルール", "xpack.csp.findings.findingsTable.findingsTableColumn.ruleSectionColumnLabel": "CISセクション", "xpack.csp.findings.groupBySelector.groupByLabel": "グループ分けの条件", "xpack.csp.findings.groupBySelector.groupByNoneLabel": "なし", @@ -18097,7 +18093,6 @@ "xpack.lens.indexPattern.cumulativeSumOf": "{name}の累積和", "xpack.lens.indexPattern.dateHistogram.autoLongerExplanation": "間隔を選択するために、Lensでは、指定された時間範囲が{targetBarSetting}詳細設定で分割され、データに最適な間隔が計算されます。たとえば、時間範囲が4日の場合、データは1時間のバケットに分割されます。バーの最大数を設定するには、{maxBarSetting}詳細設定を使用します。", "xpack.lens.indexPattern.dateHistogram.restrictedInterval": "集約の制限により間隔は {intervalValue} に固定されています。", - "xpack.lens.indexPattern.dateHistogramTimeShift": "単一のレイヤーでは、前の時間範囲シフトと日付ヒストグラムを結合できません。\"{column}\"で明示的な時間シフト期間を使用するか、日付ヒストグラムを置換してください。", "xpack.lens.indexPattern.derivativeOf": "{name} の差異", "xpack.lens.indexPattern.fieldNoOperation": "フィールド{field}は演算なしで使用できません", "xpack.lens.indexPattern.fieldsNotFound": "{count, plural, other {個のフィールド}} {missingFields} {count, plural, other {が}}見つかりません", @@ -36174,7 +36169,6 @@ "cases.components.status.inProgress": "進行中", "cases.components.status.open": "開く", "devTools.badge.betaLabel": "ベータ", - "devTools.badge.betaTooltipText": "この機能は将来のリリースで大幅に変更される可能性があります", "devTools.badge.readOnly.text": "読み取り専用", "devTools.badge.readOnly.tooltip": "を保存できませんでした", "devTools.breadcrumb.homeLabel": "開発ツール", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 87f48e38b51f5..64b94583cc67f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10139,12 +10139,10 @@ "xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundForNameTitle": " 对于“{name}”", "xpack.csp.benchmarks.totalIntegrationsCountMessage": "正在显示 {pageCount}/{totalCount, plural, other {# 个集成}}", "xpack.csp.cloudPosturePage.errorRenderer.errorDescription": "{error} {statusCode}:{body}", - "xpack.csp.complianceDashboard.complianceByCisSection.complianceColumnTooltip": "{passed}/{total}", "xpack.csp.dashboard.benchmarkSection.clusterTitle": "{title} - {shortId}", "xpack.csp.dashboard.benchmarkSection.clusterTitleTooltip.clusterTitle": "{title} - {shortId}", "xpack.csp.dashboard.benchmarkSection.lastEvaluatedTitle": "上次评估于 {dateFromNow}", "xpack.csp.findings.distributionBar.showingPageOfTotalLabel": "正在显示第 {pageStart}-{pageEnd} 个(共 {total} 个){type}", - "xpack.csp.findings.findingsByResourceTable.failedFindingsToolTip": "{failed} 个(共 {total} 个)", "xpack.csp.findings.findingsTableCell.addFilterButton": "添加 {field} 筛选", "xpack.csp.findings.findingsTableCell.addNegateFilterButton": "添加 {field} 作废筛选", "xpack.csp.findings.resourceFindings.resourceFindingsPageTitle": "{resourceName} - 结果", @@ -10201,7 +10199,6 @@ "xpack.csp.findings.findingsByResource.noFindingsTitle": "无结果", "xpack.csp.findings.findingsByResource.tableRowTypeLabel": "资源", "xpack.csp.findings.findingsByResourceTable.cisSectionsColumnLabel": "CIS 部分", - "xpack.csp.findings.findingsByResourceTable.failedFindingsColumnLabel": "失败的结果", "xpack.csp.findings.findingsErrorToast.searchFailedTitle": "搜索失败", "xpack.csp.findings.findingsFlyout.jsonTabTitle": "JSON", "xpack.csp.findings.findingsFlyout.overviewTab.actualTitle": "实际", @@ -10243,7 +10240,6 @@ "xpack.csp.findings.findingsTable.findingsTableColumn.resourceTypeColumnLabel": "资源类型", "xpack.csp.findings.findingsTable.findingsTableColumn.resultColumnLabel": "结果", "xpack.csp.findings.findingsTable.findingsTableColumn.ruleBenchmarkColumnLabel": "基准", - "xpack.csp.findings.findingsTable.findingsTableColumn.ruleColumnLabel": "规则", "xpack.csp.findings.findingsTable.findingsTableColumn.ruleSectionColumnLabel": "CIS 部分", "xpack.csp.findings.groupBySelector.groupByLabel": "分组依据", "xpack.csp.findings.groupBySelector.groupByNoneLabel": "无", @@ -18120,7 +18116,6 @@ "xpack.lens.indexPattern.cumulativeSumOf": "{name} 的累计和", "xpack.lens.indexPattern.dateHistogram.autoLongerExplanation": "要选择时间间隔,Lens 会按 {targetBarSetting} 高级设置分割指定的时间范围,并为您的数据计算最佳时间间隔。例如,当时间间隔为 4 天时,数据将分割为每小时存储桶。要配置最大条形数,请使用 {maxBarSetting} 高级设置。", "xpack.lens.indexPattern.dateHistogram.restrictedInterval": "由于聚合限制,时间间隔固定为 {intervalValue}。", - "xpack.lens.indexPattern.dateHistogramTimeShift": "在单个图层中,您无法组合上一时间范围偏移与 Date Histogram。在“{column}”中使用显式时间偏移持续时间,或替换 Date Histogram。", "xpack.lens.indexPattern.derivativeOf": "{name} 的差异", "xpack.lens.indexPattern.fieldNoOperation": "没有运算,无法使用字段 {field}", "xpack.lens.indexPattern.fieldsNotFound": "找不到{count, plural, other {字段}} {missingFields} {count, plural, other {}}", @@ -36210,7 +36205,6 @@ "cases.components.status.inProgress": "进行中", "cases.components.status.open": "打开", "devTools.badge.betaLabel": "公测版", - "devTools.badge.betaTooltipText": "此功能在未来的版本中可能会变化很大", "devTools.badge.readOnly.text": "只读", "devTools.badge.readOnly.tooltip": "无法保存", "devTools.breadcrumb.homeLabel": "开发工具", diff --git a/x-pack/test/apm_api_integration/tests/mobile/mobile_http_requests_chart.spec.ts b/x-pack/test/apm_api_integration/tests/mobile/mobile_http_requests_timeseries.spec.ts similarity index 78% rename from x-pack/test/apm_api_integration/tests/mobile/mobile_http_requests_chart.spec.ts rename to x-pack/test/apm_api_integration/tests/mobile/mobile_http_requests_timeseries.spec.ts index 33467b486704f..451a3d803d8c6 100644 --- a/x-pack/test/apm_api_integration/tests/mobile/mobile_http_requests_chart.spec.ts +++ b/x-pack/test/apm_api_integration/tests/mobile/mobile_http_requests_timeseries.spec.ts @@ -51,8 +51,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { describe('when no data', () => { it('handles empty state', async () => { const response = await getHttpRequestsChart({ serviceName: 'foo' }); - expect(response.body.currentPeriod).to.eql([]); - expect(response.body.previousPeriod).to.eql([]); + expect(response.body.currentPeriod.timeseries).to.eql([]); + expect(response.body.previousPeriod.timeseries).to.eql([]); expect(response.status).to.be(200); }); }); @@ -75,11 +75,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.be(200); expect( - response.body.currentPeriod.some( - (item: { x: number; y?: number | null }) => item.y === 0 && item.x - ) + response.body.currentPeriod.timeseries.some((item) => item.y === 0 && item.x) ).to.eql(true); - expect(response.body.previousPeriod[0].y).to.eql(0); + expect(response.body.previousPeriod.timeseries[0].y).to.eql(0); }); it('returns only current period timeseries when offset is not available', async () => { @@ -87,13 +85,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.be(200); expect( - response.body.currentPeriod.some( - (item: { x: number; y?: number | null }) => item.y === 0 && item.x - ) + response.body.currentPeriod.timeseries.some((item) => item.y === 0 && item.x) ).to.eql(true); - expect(response.body.currentPeriod[0].y).to.eql(0); - expect(response.body.previousPeriod).to.eql([]); + expect(response.body.currentPeriod.timeseries[0].y).to.eql(0); + expect(response.body.previousPeriod.timeseries).to.eql([]); }); }); @@ -106,16 +102,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); expect(response.status).to.be(200); - expect( - response.body.currentPeriod.every( - (item: { x: number; y?: number | null }) => item.y === 0 - ) - ).to.eql(true); - expect( - response.body.previousPeriod.every( - (item: { x: number; y?: number | null }) => item.y === 0 - ) - ).to.eql(true); + expect(response.body.currentPeriod.timeseries.every((item) => item.y === 0)).to.eql(true); + expect(response.body.previousPeriod.timeseries.every((item) => item.y === 0)).to.eql(true); }); it('returns the correct values when filter is applied', async () => { @@ -133,8 +121,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.be(200); expect(ntcCell.status).to.be(200); - expect(response.body.currentPeriod[0].y).to.eql(0); - expect(ntcCell.body.currentPeriod[0].y).to.eql(0); + expect(response.body.currentPeriod.timeseries[0].y).to.eql(0); + expect(ntcCell.body.currentPeriod.timeseries[0].y).to.eql(0); }); }); }); diff --git a/x-pack/test/apm_api_integration/tests/mobile/mobile_sessions_chart.spec.ts b/x-pack/test/apm_api_integration/tests/mobile/mobile_sessions_timeseries.spec.ts similarity index 71% rename from x-pack/test/apm_api_integration/tests/mobile/mobile_sessions_chart.spec.ts rename to x-pack/test/apm_api_integration/tests/mobile/mobile_sessions_timeseries.spec.ts index fe3c9863af8d5..aeb6122cd7d16 100644 --- a/x-pack/test/apm_api_integration/tests/mobile/mobile_sessions_chart.spec.ts +++ b/x-pack/test/apm_api_integration/tests/mobile/mobile_sessions_timeseries.spec.ts @@ -51,8 +51,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { describe('when no data', () => { it('handles empty state', async () => { const response = await getSessionsChart({ serviceName: 'foo' }); - expect(response.body.currentPeriod).to.eql([]); - expect(response.body.previousPeriod).to.eql([]); + expect(response.body.currentPeriod.timeseries).to.eql([]); + expect(response.body.previousPeriod.timeseries).to.eql([]); expect(response.status).to.be(200); }); }); @@ -74,28 +74,24 @@ export default function ApiTest({ getService }: FtrProviderContext) { const response = await getSessionsChart({ serviceName: 'synth-android', offset: '1d' }); expect(response.status).to.be(200); - expect( - response.body.currentPeriod.some( - (item: { x: number; y?: number | null }) => item.x && item.y - ) - ).to.eql(true); - - expect(response.body.currentPeriod[0].y).to.eql(8); - expect(response.body.previousPeriod[0].y).to.eql(0); + expect(response.body.currentPeriod.timeseries.some((item) => item.x && item.y)).to.eql( + true + ); + + expect(response.body.currentPeriod.timeseries[0].y).to.eql(2); + expect(response.body.previousPeriod.timeseries[0].y).to.eql(0); }); it('returns only current period timeseries when offset is not available', async () => { const response = await getSessionsChart({ serviceName: 'synth-android' }); expect(response.status).to.be(200); - expect( - response.body.currentPeriod.some( - (item: { x: number; y?: number | null }) => item.x && item.y - ) - ).to.eql(true); - - expect(response.body.currentPeriod[0].y).to.eql(8); - expect(response.body.previousPeriod).to.eql([]); + expect(response.body.currentPeriod.timeseries.some((item) => item.x && item.y)).to.eql( + true + ); + + expect(response.body.currentPeriod.timeseries[0].y).to.eql(2); + expect(response.body.previousPeriod.timeseries).to.eql([]); }); }); @@ -107,16 +103,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { kuery: `app.version:"none"`, }); - expect( - response.body.currentPeriod.every( - (item: { x: number; y?: number | null }) => item.y === 0 - ) - ).to.eql(true); - expect( - response.body.previousPeriod.every( - (item: { x: number; y?: number | null }) => item.y === 0 - ) - ).to.eql(true); + expect(response.body.currentPeriod.timeseries.every((item) => item.y === 0)).to.eql(true); + expect(response.body.previousPeriod.timeseries.every((item) => item.y === 0)).to.eql(true); }); it('returns the correct values filter is applied', async () => { @@ -127,14 +115,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); expect(response.status).to.be(200); - expect( - response.body.currentPeriod.some( - (item: { x: number; y?: number | null }) => item.x && item.y - ) - ).to.eql(true); - - expect(response.body.currentPeriod[0].y).to.eql(2); - expect(response.body.previousPeriod).to.eql([]); + expect(response.body.currentPeriod.timeseries.some((item) => item.x && item.y)).to.eql( + true + ); + + expect(response.body.currentPeriod.timeseries[0].y).to.eql(2); + expect(response.body.previousPeriod.timeseries).to.eql([]); }); }); }); diff --git a/x-pack/test/apm_api_integration/tests/mobile/mobile_stats.spec.ts b/x-pack/test/apm_api_integration/tests/mobile/mobile_stats.spec.ts index 32ff956a70007..77508a34d7ddd 100644 --- a/x-pack/test/apm_api_integration/tests/mobile/mobile_stats.spec.ts +++ b/x-pack/test/apm_api_integration/tests/mobile/mobile_stats.spec.ts @@ -54,21 +54,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { describe('when no data', () => { it('handles empty state', async () => { const response = await getMobileStats({ serviceName: 'foo' }); - expect(response).to.eql({ - sessions: { - timeseries: [], - }, - requests: { - timeseries: [], - }, - maxLoadTime: { - timeseries: [], - }, - crashCount: { - value: 0, - timeseries: [], - }, - }); + expect(response.currentPeriod.sessions.timeseries.every((item) => item.y === 0)).to.eql( + true + ); + expect(response.currentPeriod.requests.timeseries.every((item) => item.y === 0)).to.eql( + true + ); }); }); }); @@ -95,19 +86,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns same sessions', () => { - const { value, timeseries } = response.sessions; - const timeseriesTotal = sumBy(timeseries, 'y'); - expect(value).to.be(timeseriesTotal); - }); - - it('returns same crashCount', () => { - const { value, timeseries } = response.crashCount; + const { value, timeseries } = response.currentPeriod.sessions; const timeseriesTotal = sumBy(timeseries, 'y'); expect(value).to.be(timeseriesTotal); }); it('returns same requests', () => { - const { value, timeseries } = response.requests; + const { value, timeseries } = response.currentPeriod.requests; const timeseriesTotal = sumBy(timeseries, 'y'); expect(value).to.be(timeseriesTotal); }); @@ -121,49 +106,36 @@ export default function ApiTest({ getService }: FtrProviderContext) { kuery: `app.version:"none"`, }); - expect(response).to.eql({ - sessions: { - value: 0, - timeseries: [], - }, - requests: { - value: 0, - timeseries: [], - }, - maxLoadTime: { - value: null, - timeseries: [], - }, - crashCount: { - value: 0, - timeseries: [], - }, - }); + expect(response.currentPeriod.sessions.value).to.eql(0); + expect(response.currentPeriod.requests.value).to.eql(0); + + expect(response.currentPeriod.sessions.timeseries.every((item) => item.y === 0)).to.eql( + true + ); + expect(response.currentPeriod.requests.timeseries.every((item) => item.y === 0)).to.eql( + true + ); }); it('returns the correct values when single filter is applied', async () => { const response = await getMobileStats({ serviceName: 'synth-android', environment: 'production', - kuery: `network.connection.type:"wifi"`, + kuery: `service.version:"1.0"`, }); - expect(response.sessions.value).to.eql(3); - expect(response.requests.value).to.eql(0); - expect(response.crashCount.value).to.eql(0); - expect(response.maxLoadTime.value).to.eql(null); + expect(response.currentPeriod.sessions.value).to.eql(6); + expect(response.currentPeriod.requests.value).to.eql(0); }); it('returns the correct values when multiple filters are applied', async () => { const response = await getMobileStats({ serviceName: 'synth-android', - kuery: `app.version:"1.0" and environment: "production"`, + kuery: `service.version:"1.0" and service.environment: "production"`, }); - expect(response.sessions.value).to.eql(0); - expect(response.requests.value).to.eql(0); - expect(response.crashCount.value).to.eql(0); - expect(response.maxLoadTime.value).to.eql(null); + expect(response.currentPeriod.sessions.value).to.eql(6); + expect(response.currentPeriod.requests.value).to.eql(0); }); }); }); diff --git a/x-pack/test/cloud_security_posture_functional/pages/findings.ts b/x-pack/test/cloud_security_posture_functional/pages/findings.ts index 3cfbc0234388a..afba99a5542ed 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/findings.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/findings.ts @@ -88,7 +88,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await filterBar.addFilter({ field: 'rule.name', operation: 'is', value: ruleName1 }); expect(await filterBar.hasFilter('rule.name', ruleName1)).to.be(true); - expect(await table.hasColumnValue('Rule', ruleName1)).to.be(true); + expect(await table.hasColumnValue('Rule Name', ruleName1)).to.be(true); }); it('remove filter', async () => { @@ -102,8 +102,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await queryBar.setQuery(ruleName1); await queryBar.submitQuery(); - expect(await table.hasColumnValue('Rule', ruleName1)).to.be(true); - expect(await table.hasColumnValue('Rule', ruleName2)).to.be(false); + expect(await table.hasColumnValue('Rule Name', ruleName1)).to.be(true); + expect(await table.hasColumnValue('Rule Name', ruleName2)).to.be(false); await queryBar.setQuery(''); await queryBar.submitQuery(); @@ -114,18 +114,18 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('Table Filters', () => { it('add cell value filter', async () => { - await table.addCellFilter('Rule', ruleName1, false); + await table.addCellFilter('Rule Name', ruleName1, false); expect(await filterBar.hasFilter('rule.name', ruleName1)).to.be(true); - expect(await table.hasColumnValue('Rule', ruleName1)).to.be(true); + expect(await table.hasColumnValue('Rule Name', ruleName1)).to.be(true); }); it('add negated cell value filter', async () => { - await table.addCellFilter('Rule', ruleName1, true); + await table.addCellFilter('Rule Name', ruleName1, true); expect(await filterBar.hasFilter('rule.name', ruleName1, true, false, true)).to.be(true); - expect(await table.hasColumnValue('Rule', ruleName1)).to.be(false); - expect(await table.hasColumnValue('Rule', ruleName2)).to.be(true); + expect(await table.hasColumnValue('Rule Name', ruleName1)).to.be(false); + expect(await table.hasColumnValue('Rule Name', ruleName2)).to.be(true); await filterBar.removeFilter('rule.name'); }); @@ -147,8 +147,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testCases: TestCase[] = [ ['CIS Section', 'asc', sortByAlphabeticalOrder], ['CIS Section', 'desc', sortByAlphabeticalOrder], - ['Cluster ID', 'asc', compareStringByLexicographicOrder], - ['Cluster ID', 'desc', compareStringByLexicographicOrder], + ['Resource ID', 'asc', compareStringByLexicographicOrder], + ['Resource ID', 'desc', compareStringByLexicographicOrder], ['Resource Name', 'asc', sortByAlphabeticalOrder], ['Resource Name', 'desc', sortByAlphabeticalOrder], ['Resource Type', 'asc', sortByAlphabeticalOrder],