diff --git a/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap index 1e340f6ffc755c..508d8aac06dad9 100644 --- a/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap @@ -1075,6 +1075,9 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the "properties": { "kuery_fields": { "type": "keyword" + }, + "total": { + "type": "long" } } }, diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts index b69aa1e6e01961..bf03b999f1046d 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks'; import { ApmIndicesConfig } from '../../../routes/settings/apm_indices/get_apm_indices'; import { tasks } from './tasks'; import { @@ -444,4 +445,49 @@ describe('data telemetry collection tasks', () => { }); }); }); + + describe('service groups', () => { + const task = tasks.find((t) => t.name === 'service_groups'); + const savedObjectsClient = savedObjectsClientMock.create(); + + savedObjectsClient.find.mockResolvedValue({ + page: 1, + per_page: 500, + total: 2, + saved_objects: [ + { + type: 'apm-service-group', + id: '0b6157f0-44bd-11ed-bdb7-bffab551cd4d', + namespaces: ['default'], + attributes: { + color: '#5094C4', + kuery: 'service.environment: production', + groupName: 'production', + }, + references: [], + score: 1, + }, + { + type: 'apm-service-group', + id: '0b6157f0-44bd-11ed-bdb7-bffab551cd4d', + namespaces: ['default'], + attributes: { + color: '#5094C4', + kuery: 'agent.name: go', + groupName: 'agent', + }, + references: [], + score: 0, + }, + ], + }); + it('returns service group stats', async () => { + expect(await task?.executor({ savedObjectsClient } as any)).toEqual({ + service_groups: { + kuery_fields: ['service.environment', 'agent.name'], + total: 2, + }, + }); + }); + }); }); diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts index 430930fb3f9163..12e89ef32832ce 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts @@ -15,6 +15,7 @@ import { AGENT_NAMES, RUM_AGENT_NAMES } from '../../../../common/agent_name'; import { SavedServiceGroup, APM_SERVICE_GROUP_SAVED_OBJECT_TYPE, + MAX_NUMBER_OF_SERVICE_GROUPS, } from '../../../../common/service_groups'; import { getKueryFields } from '../../helpers/get_kuery_fields'; import { @@ -1134,7 +1135,7 @@ export const tasks: TelemetryTask[] = [ const response = await savedObjectsClient.find({ type: APM_SERVICE_GROUP_SAVED_OBJECT_TYPE, page: 1, - perPage: 50, + perPage: MAX_NUMBER_OF_SERVICE_GROUPS, sortField: 'updated_at', sortOrder: 'desc', }); @@ -1148,6 +1149,7 @@ export const tasks: TelemetryTask[] = [ return { service_groups: { kuery_fields: kueryFields, + total: response.total ?? 0, }, }; }, diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts index f33c9e03f6caab..4430389785e1d6 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts @@ -235,6 +235,7 @@ export const apmSchema: MakeSchemaFrom = { }, service_groups: { kuery_fields: { type: 'array', items: { type: 'keyword' } }, + total: long, }, per_service: { type: 'array', items: { ...apmPerServiceSchema } }, tasks: { diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts index 16e1b926d578ef..ceadcbfc1ded24 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts @@ -173,6 +173,7 @@ export interface APMUsage { }; service_groups: { kuery_fields: string[]; + total: number; }; per_service: APMPerService[]; tasks: Record< diff --git a/x-pack/plugins/lens/common/embeddable_factory/index.ts b/x-pack/plugins/lens/common/embeddable_factory/index.ts index 8ddddf654b017b..b794ec642f40a9 100644 --- a/x-pack/plugins/lens/common/embeddable_factory/index.ts +++ b/x-pack/plugins/lens/common/embeddable_factory/index.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { SerializableRecord, Serializable } from '@kbn/utility-types'; -import { SavedObjectReference } from '@kbn/core/types'; +import type { SerializableRecord, Serializable } from '@kbn/utility-types'; +import type { SavedObjectReference } from '@kbn/core/types'; import type { EmbeddableStateWithType, EmbeddableRegistryDefinition, diff --git a/x-pack/plugins/lens/common/expressions/time_scale/time_scale.test.ts b/x-pack/plugins/lens/common/expressions/time_scale/time_scale.test.ts index b9b4fb48899947..fceabbd5542b37 100644 --- a/x-pack/plugins/lens/common/expressions/time_scale/time_scale.test.ts +++ b/x-pack/plugins/lens/common/expressions/time_scale/time_scale.test.ts @@ -12,18 +12,6 @@ import type { TimeRange } from '@kbn/es-query'; import { createDatatableUtilitiesMock } from '@kbn/data-plugin/common/mocks'; import { functionWrapper } from '@kbn/expressions-plugin/common/expression_functions/specs/tests/utils'; -// mock the specific inner variable: -// there are intra dependencies in the data plugin we might break trying to mock the whole thing -jest.mock('@kbn/data-plugin/common/query/timefilter/get_time', () => { - const localMoment = jest.requireActual('moment'); - return { - calculateBounds: jest.fn(({ from, to }) => ({ - min: localMoment(from), - max: localMoment(to), - })), - }; -}); - import { getTimeScale } from './time_scale'; import type { TimeScaleArgs } from './types'; @@ -425,6 +413,35 @@ describe('time_scale', () => { expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([75]); }); + it('should work with relative time range', async () => { + const result = await timeScaleWrapped( + { + ...emptyTable, + rows: [ + { + date: moment().subtract('1d').valueOf(), + metric: 300, + }, + ], + }, + { + inputColumnId: 'metric', + outputColumnId: 'scaledMetric', + targetUnit: 'd', + }, + { + getSearchContext: () => ({ + timeRange: { + from: 'now-10d', + to: 'now', + }, + }), + } as unknown as ExecutionContext + ); + + expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([30]); + }); + it('should apply fn for non-histogram fields (with Reduced time range)', async () => { const result = await timeScaleWrapped( { diff --git a/x-pack/plugins/lens/common/expressions/time_scale/time_scale_fn.ts b/x-pack/plugins/lens/common/expressions/time_scale/time_scale_fn.ts index 722056321e17a9..6b2e1857a902cc 100644 --- a/x-pack/plugins/lens/common/expressions/time_scale/time_scale_fn.ts +++ b/x-pack/plugins/lens/common/expressions/time_scale/time_scale_fn.ts @@ -24,12 +24,39 @@ const unitInMs: Record = { d: 1000 * 60 * 60 * 24, }; +// the datemath plugin always parses dates by using the current default moment time zone. +// to use the configured time zone, we are temporary switching it just for the calculation. + +// The code between this call and the reset in the finally block is not allowed to get async, +// otherwise the timezone setting can leak out of this function. +const withChangedTimeZone = ( + timeZone: string | undefined, + action: () => TReturnedValue +): TReturnedValue => { + if (timeZone) { + const defaultTimezone = moment().zoneName(); + try { + moment.tz.setDefault(timeZone); + return action(); + } finally { + // reset default moment timezone + moment.tz.setDefault(defaultTimezone); + } + } else { + return action(); + } +}; + +const getTimeBounds = (timeRange: TimeRange, timeZone?: string, getForceNow?: () => Date) => + withChangedTimeZone(timeZone, () => calculateBounds(timeRange, { forceNow: getForceNow?.() })); + export const timeScaleFn = ( getDatatableUtilities: ( context: ExecutionContext ) => DatatableUtilitiesService | Promise, - getTimezone: (context: ExecutionContext) => string | Promise + getTimezone: (context: ExecutionContext) => string | Promise, + getForceNow?: () => Date ): TimeScaleExpressionFunction['fn'] => async ( input, @@ -69,7 +96,9 @@ export const timeScaleFn = timeZone: contextTimeZone, }); const intervalDuration = timeInfo?.interval && parseInterval(timeInfo.interval); - timeBounds = timeInfo?.timeRange && calculateBounds(timeInfo.timeRange); + + timeBounds = + timeInfo?.timeRange && getTimeBounds(timeInfo.timeRange, timeInfo?.timeZone, getForceNow); getStartEndOfBucketMeta = (row) => { const startOfBucket = moment.tz(row[dateColumnId], timeInfo?.timeZone ?? contextTimeZone); @@ -89,8 +118,18 @@ export const timeScaleFn = } } else { const timeRange = context.getSearchContext().timeRange as TimeRange; - const endOfBucket = moment.tz(timeRange.to, contextTimeZone); - let startOfBucket = moment.tz(timeRange.from, contextTimeZone); + timeBounds = getTimeBounds(timeRange, contextTimeZone, getForceNow); + + if (!timeBounds.max || !timeBounds.min) { + throw new Error( + i18n.translate('xpack.lens.functions.timeScale.timeBoundsMissingMessage', { + defaultMessage: 'Could not parse "Time Range"', + }) + ); + } + + const endOfBucket = timeBounds.max; + let startOfBucket = timeBounds.min; if (reducedTimeRange) { const reducedStartOfBucket = endOfBucket.clone().subtract(parseInterval(reducedTimeRange)); @@ -100,8 +139,6 @@ export const timeScaleFn = } } - timeBounds = calculateBounds(timeRange); - getStartEndOfBucketMeta = () => ({ startOfBucket, endOfBucket, diff --git a/x-pack/plugins/lens/common/constants.test.ts b/x-pack/plugins/lens/common/helpers.test.ts similarity index 100% rename from x-pack/plugins/lens/common/constants.test.ts rename to x-pack/plugins/lens/common/helpers.test.ts diff --git a/x-pack/plugins/lens/common/index.ts b/x-pack/plugins/lens/common/index.ts index 7929ebf6d17840..45dd41422b0189 100644 --- a/x-pack/plugins/lens/common/index.ts +++ b/x-pack/plugins/lens/common/index.ts @@ -10,7 +10,6 @@ export * from './constants'; export * from './types'; -export * from './visualizations'; // Note: do not import the expression folder here or the page bundle will be bloated with all // the package diff --git a/x-pack/plugins/lens/common/types.ts b/x-pack/plugins/lens/common/types.ts index c21da9e31475c6..d70ee4b7025a0b 100644 --- a/x-pack/plugins/lens/common/types.ts +++ b/x-pack/plugins/lens/common/types.ts @@ -6,12 +6,12 @@ */ import type { Filter, FilterMeta } from '@kbn/es-query'; -import { Position } from '@elastic/charts'; -import { $Values } from '@kbn/utility-types'; +import type { Position } from '@elastic/charts'; +import type { $Values } from '@kbn/utility-types'; import type { CustomPaletteParams, PaletteOutput } from '@kbn/coloring'; import type { IFieldFormat, SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; import type { ColorMode } from '@kbn/charts-plugin/common'; -import { LegendSize } from '@kbn/visualizations-plugin/common'; +import type { LegendSize } from '@kbn/visualizations-plugin/common'; import { CategoryDisplay, layerTypes, diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts index d96c2aee26c03d..cef9e7d698faa5 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts @@ -23,16 +23,16 @@ import { IContainer, ErrorEmbeddable, } from '@kbn/embeddable-plugin/public'; -import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import { Start as InspectorStart } from '@kbn/inspector-plugin/public'; +import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import type { Start as InspectorStart } from '@kbn/inspector-plugin/public'; import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; -import { LensByReferenceInput, LensEmbeddableInput } from './embeddable'; -import { Document } from '../persistence/saved_object_store'; -import { LensAttributeService } from '../lens_attribute_service'; +import type { LensByReferenceInput, LensEmbeddableInput } from './embeddable'; +import type { Document } from '../persistence/saved_object_store'; +import type { LensAttributeService } from '../lens_attribute_service'; import { DOC_TYPE } from '../../common/constants'; -import { ErrorMessage } from '../editor_frame_service/types'; +import type { ErrorMessage } from '../editor_frame_service/types'; import { extract, inject } from '../../common/embeddable_factory'; -import { DatasourceMap, VisualizationMap } from '../types'; +import type { DatasourceMap, VisualizationMap } from '../types'; export interface LensEmbeddableStartServices { data: DataPublicPluginStart; diff --git a/x-pack/plugins/lens/public/expressions.ts b/x-pack/plugins/lens/public/expressions.ts index a5f193d63e4f34..bfad4e70cf1c70 100644 --- a/x-pack/plugins/lens/public/expressions.ts +++ b/x-pack/plugins/lens/public/expressions.ts @@ -6,6 +6,7 @@ */ import type { ExpressionsSetup } from '@kbn/expressions-plugin/public'; + import { getDatatable } from '../common/expressions/datatable/datatable'; import { datatableColumn } from '../common/expressions/datatable/datatable_column'; import { mapToColumns } from '../common/expressions/map_to_columns/map_to_columns'; @@ -14,11 +15,14 @@ import { counterRate } from '../common/expressions/counter_rate'; import { getTimeScale } from '../common/expressions/time_scale/time_scale'; import { collapse } from '../common/expressions'; +type TimeScaleArguments = Parameters; + export const setupExpressions = ( expressions: ExpressionsSetup, formatFactory: Parameters[0], - getDatatableUtilities: Parameters[0], - getTimeZone: Parameters[1] + getDatatableUtilities: TimeScaleArguments[0], + getTimeZone: TimeScaleArguments[1], + getForceNow: TimeScaleArguments[2] ) => { [ collapse, @@ -27,6 +31,6 @@ export const setupExpressions = ( mapToColumns, datatableColumn, getDatatable(formatFactory), - getTimeScale(getDatatableUtilities, getTimeZone), + getTimeScale(getDatatableUtilities, getTimeZone, getForceNow), ].forEach((expressionFn) => expressions.registerFunction(expressionFn)); }; diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 6019f566e0ba68..10465a88f1623a 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -334,7 +334,8 @@ export class LensPlugin { async () => { const { getTimeZone } = await import('./utils'); return getTimeZone(core.uiSettings); - } + }, + () => startServices().plugins.data.nowProvider.get() ); const getPresentationUtilContext = () => diff --git a/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts b/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts index 9f2d0920983ad2..7a0c94d5d1b33c 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts @@ -19,8 +19,8 @@ import { NumberDisplay, PieChartTypes, PieVisualizationState, - isPartitionShape, } from '../../../common'; +import { isPartitionShape } from '../../../common/visualizations'; import type { PieChartType } from '../../../common/types'; import { PartitionChartsMeta } from './partition_charts_meta'; diff --git a/x-pack/plugins/lens/server/migrations/common_migrations.ts b/x-pack/plugins/lens/server/migrations/common_migrations.ts index 7fd65d44bb0b6c..282308753698ea 100644 --- a/x-pack/plugins/lens/server/migrations/common_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/common_migrations.ts @@ -34,7 +34,8 @@ import { VisState850, LensDocShape850, } from './types'; -import { DOCUMENT_FIELD_NAME, layerTypes, LegacyMetricState, isPartitionShape } from '../../common'; +import { DOCUMENT_FIELD_NAME, layerTypes, LegacyMetricState } from '../../common'; +import { isPartitionShape } from '../../common/visualizations'; import { LensDocShape } from './saved_object_migrations'; export const commonRenameOperationsForFormula = ( diff --git a/x-pack/plugins/profiling/public/components/flame_graphs_view/index.tsx b/x-pack/plugins/profiling/public/components/flame_graphs_view/index.tsx index 6a8802599f6cc1..dab912902ba460 100644 --- a/x-pack/plugins/profiling/public/components/flame_graphs_view/index.tsx +++ b/x-pack/plugins/profiling/public/components/flame_graphs_view/index.tsx @@ -43,35 +43,40 @@ export function FlameGraphsView({ children }: { children: React.ReactElement }) services: { fetchElasticFlamechart }, } = useProfilingDependencies(); - const state = useTimeRangeAsync(() => { - return Promise.all([ - fetchElasticFlamechart({ - timeFrom: new Date(timeRange.start).getTime() / 1000, - timeTo: new Date(timeRange.end).getTime() / 1000, - kuery, - }), - comparisonTimeRange.start && comparisonTimeRange.end - ? fetchElasticFlamechart({ - timeFrom: new Date(comparisonTimeRange.start).getTime() / 1000, - timeTo: new Date(comparisonTimeRange.end).getTime() / 1000, - kuery: comparisonKuery, - }) - : Promise.resolve(undefined), - ]).then(([primaryFlamegraph, comparisonFlamegraph]) => { - return { - primaryFlamegraph, - comparisonFlamegraph, - }; - }); - }, [ - timeRange.start, - timeRange.end, - kuery, - comparisonTimeRange.start, - comparisonTimeRange.end, - comparisonKuery, - fetchElasticFlamechart, - ]); + const state = useTimeRangeAsync( + ({ http }) => { + return Promise.all([ + fetchElasticFlamechart({ + http, + timeFrom: new Date(timeRange.start).getTime() / 1000, + timeTo: new Date(timeRange.end).getTime() / 1000, + kuery, + }), + comparisonTimeRange.start && comparisonTimeRange.end + ? fetchElasticFlamechart({ + http, + timeFrom: new Date(comparisonTimeRange.start).getTime() / 1000, + timeTo: new Date(comparisonTimeRange.end).getTime() / 1000, + kuery: comparisonKuery, + }) + : Promise.resolve(undefined), + ]).then(([primaryFlamegraph, comparisonFlamegraph]) => { + return { + primaryFlamegraph, + comparisonFlamegraph, + }; + }); + }, + [ + timeRange.start, + timeRange.end, + kuery, + comparisonTimeRange.start, + comparisonTimeRange.end, + comparisonKuery, + fetchElasticFlamechart, + ] + ); const { data } = state; @@ -173,7 +178,6 @@ export function FlameGraphsView({ children }: { children: React.ReactElement }) = ({ id, - height, comparisonMode, primaryFlamegraph, comparisonFlamegraph, diff --git a/x-pack/plugins/profiling/public/components/functions_view/index.tsx b/x-pack/plugins/profiling/public/components/functions_view/index.tsx index 94410f961c469b..2c7b25754f3d9d 100644 --- a/x-pack/plugins/profiling/public/components/functions_view/index.tsx +++ b/x-pack/plugins/profiling/public/components/functions_view/index.tsx @@ -42,28 +42,36 @@ export function FunctionsView({ children }: { children: React.ReactElement }) { services: { fetchTopNFunctions }, } = useProfilingDependencies(); - const state = useTimeRangeAsync(() => { - return fetchTopNFunctions({ - timeFrom: new Date(timeRange.start).getTime() / 1000, - timeTo: new Date(timeRange.end).getTime() / 1000, - startIndex: 0, - endIndex: 1000, - kuery, - }); - }, [timeRange.start, timeRange.end, kuery, fetchTopNFunctions]); + const state = useTimeRangeAsync( + ({ http }) => { + return fetchTopNFunctions({ + http, + timeFrom: new Date(timeRange.start).getTime() / 1000, + timeTo: new Date(timeRange.end).getTime() / 1000, + startIndex: 0, + endIndex: 1000, + kuery, + }); + }, + [timeRange.start, timeRange.end, kuery, fetchTopNFunctions] + ); - const comparisonState = useTimeRangeAsync(() => { - if (!comparisonTimeRange.start || !comparisonTimeRange.end) { - return undefined; - } - return fetchTopNFunctions({ - timeFrom: new Date(comparisonTimeRange.start).getTime() / 1000, - timeTo: new Date(comparisonTimeRange.end).getTime() / 1000, - startIndex: 0, - endIndex: 1000, - kuery: comparisonKuery, - }); - }, [comparisonTimeRange.start, comparisonTimeRange.end, comparisonKuery, fetchTopNFunctions]); + const comparisonState = useTimeRangeAsync( + ({ http }) => { + if (!comparisonTimeRange.start || !comparisonTimeRange.end) { + return undefined; + } + return fetchTopNFunctions({ + http, + timeFrom: new Date(comparisonTimeRange.start).getTime() / 1000, + timeTo: new Date(comparisonTimeRange.end).getTime() / 1000, + startIndex: 0, + endIndex: 1000, + kuery: comparisonKuery, + }); + }, + [comparisonTimeRange.start, comparisonTimeRange.end, comparisonKuery, fetchTopNFunctions] + ); const routePath = useProfilingRoutePath() as | '/functions' diff --git a/x-pack/plugins/profiling/public/components/stack_traces_view/index.tsx b/x-pack/plugins/profiling/public/components/stack_traces_view/index.tsx index 23bb4a7bd4b6fa..19834410c6b7ac 100644 --- a/x-pack/plugins/profiling/public/components/stack_traces_view/index.tsx +++ b/x-pack/plugins/profiling/public/components/stack_traces_view/index.tsx @@ -50,29 +50,33 @@ export function StackTracesView() { rangeTo, }); - const state = useTimeRangeAsync(() => { - if (!topNType) { - return Promise.resolve({ charts: [], metadata: {} }); - } - return fetchTopN({ - type: topNType, - timeFrom: new Date(timeRange.start).getTime() / 1000, - timeTo: new Date(timeRange.end).getTime() / 1000, - kuery, - }).then((response: TopNResponse) => { - const totalCount = response.TotalCount; - const samples = response.TopN; - const charts = groupSamplesByCategory({ - samples, - totalCount, - metadata: response.Metadata, - labels: response.Labels, + const state = useTimeRangeAsync( + ({ http }) => { + if (!topNType) { + return Promise.resolve({ charts: [], metadata: {} }); + } + return fetchTopN({ + http, + type: topNType, + timeFrom: new Date(timeRange.start).getTime() / 1000, + timeTo: new Date(timeRange.end).getTime() / 1000, + kuery, + }).then((response: TopNResponse) => { + const totalCount = response.TotalCount; + const samples = response.TopN; + const charts = groupSamplesByCategory({ + samples, + totalCount, + metadata: response.Metadata, + labels: response.Labels, + }); + return { + charts, + }; }); - return { - charts, - }; - }); - }, [topNType, timeRange.start, timeRange.end, fetchTopN, kuery]); + }, + [topNType, timeRange.start, timeRange.end, fetchTopN, kuery] + ); const [highlightedSubchart, setHighlightedSubchart] = useState( undefined diff --git a/x-pack/plugins/profiling/public/hooks/use_async.ts b/x-pack/plugins/profiling/public/hooks/use_async.ts index ea6da578c3879a..7156b4e8bfffd5 100644 --- a/x-pack/plugins/profiling/public/hooks/use_async.ts +++ b/x-pack/plugins/profiling/public/hooks/use_async.ts @@ -4,8 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { HttpStart } from '@kbn/core-http-browser'; -import { useEffect, useState } from 'react'; +import { HttpFetchOptions, HttpHandler, HttpStart } from '@kbn/core-http-browser'; +import { AbortError } from '@kbn/kibana-utils-plugin/common'; +import { useEffect, useRef, useState } from 'react'; +import { Overwrite, ValuesType } from 'utility-types'; import { useProfilingDependencies } from '../components/contexts/profiling_dependencies/use_profiling_dependencies'; export enum AsyncStatus { @@ -20,8 +22,22 @@ export interface AsyncState { status: AsyncStatus; } +const HTTP_METHODS = ['fetch', 'get', 'post', 'put', 'delete', 'patch'] as const; + +type HttpMethod = ValuesType; + +type AutoAbortedHttpMethod = ( + path: string, + options: Omit +) => ReturnType; + +export type AutoAbortedHttpService = Overwrite< + HttpStart, + Record +>; + export type UseAsync = ( - fn: ({ http }: { http: HttpStart }) => Promise | undefined, + fn: ({ http }: { http: AutoAbortedHttpService }) => Promise | undefined, dependencies: any[] ) => AsyncState; @@ -37,8 +53,30 @@ export const useAsync: UseAsync = (fn, dependencies) => { const { data, error } = asyncState; + const controllerRef = useRef(new AbortController()); + useEffect(() => { - const returnValue = fn({ http }); + controllerRef.current.abort(); + + controllerRef.current = new AbortController(); + + const autoAbortedMethods = {} as Record; + + for (const key of HTTP_METHODS) { + autoAbortedMethods[key] = (path, options) => { + return http[key](path, { ...options, signal: controllerRef.current.signal }).catch( + (err) => { + if (err.name === 'AbortError') { + // return never-resolving promise + return new Promise(() => {}); + } + throw err; + } + ); + }; + } + + const returnValue = fn({ http: { ...http, ...autoAbortedMethods } }); if (returnValue === undefined) { setAsyncState({ @@ -63,13 +101,23 @@ export const useAsync: UseAsync = (fn, dependencies) => { }); returnValue.catch((nextError) => { + if (nextError instanceof AbortError) { + return; + } setAsyncState({ status: AsyncStatus.Settled, error: nextError, }); + throw nextError; }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [http, ...dependencies]); + useEffect(() => { + return () => { + controllerRef.current.abort(); + }; + }, []); + return asyncState; }; diff --git a/x-pack/plugins/profiling/public/plugin.tsx b/x-pack/plugins/profiling/public/plugin.tsx index 080941729b0143..ead023a079e29f 100644 --- a/x-pack/plugins/profiling/public/plugin.tsx +++ b/x-pack/plugins/profiling/public/plugin.tsx @@ -90,7 +90,7 @@ export class ProfilingPlugin implements Plugin { unknown ]; - const profilingFetchServices = getServices(coreStart); + const profilingFetchServices = getServices(); const { renderApp } = await import('./app'); function pushKueryToSubject(location: Location) { diff --git a/x-pack/plugins/profiling/public/services.ts b/x-pack/plugins/profiling/public/services.ts index a1f345dc96a2c2..4dcad550d0bbb4 100644 --- a/x-pack/plugins/profiling/public/services.ts +++ b/x-pack/plugins/profiling/public/services.ts @@ -4,21 +4,23 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { CoreStart, HttpFetchQuery } from '@kbn/core/public'; +import { HttpFetchQuery } from '@kbn/core/public'; import { getRoutePaths } from '../common'; import { BaseFlameGraph, createFlameGraph, ElasticFlameGraph } from '../common/flamegraph'; import { TopNFunctions } from '../common/functions'; import { TopNResponse } from '../common/topn'; +import { AutoAbortedHttpService } from './hooks/use_async'; export interface Services { fetchTopN: (params: { + http: AutoAbortedHttpService; type: string; timeFrom: number; timeTo: number; kuery: string; }) => Promise; fetchTopNFunctions: (params: { + http: AutoAbortedHttpService; timeFrom: number; timeTo: number; startIndex: number; @@ -26,42 +28,31 @@ export interface Services { kuery: string; }) => Promise; fetchElasticFlamechart: (params: { + http: AutoAbortedHttpService; timeFrom: number; timeTo: number; kuery: string; }) => Promise; } -export function getServices(core: CoreStart): Services { +export function getServices(): Services { const paths = getRoutePaths(); return { - fetchTopN: async ({ type, timeFrom, timeTo, kuery }) => { + fetchTopN: async ({ http, type, timeFrom, timeTo, kuery }) => { try { const query: HttpFetchQuery = { timeFrom, timeTo, kuery, }; - return await core.http.get(`${paths.TopN}/${type}`, { query }); + return await http.get(`${paths.TopN}/${type}`, { query }); } catch (e) { return e; } }, - fetchTopNFunctions: async ({ - timeFrom, - timeTo, - startIndex, - endIndex, - kuery, - }: { - timeFrom: number; - timeTo: number; - startIndex: number; - endIndex: number; - kuery: string; - }) => { + fetchTopNFunctions: async ({ http, timeFrom, timeTo, startIndex, endIndex, kuery }) => { try { const query: HttpFetchQuery = { timeFrom, @@ -70,28 +61,20 @@ export function getServices(core: CoreStart): Services { endIndex, kuery, }; - return await core.http.get(paths.TopNFunctions, { query }); + return await http.get(paths.TopNFunctions, { query }); } catch (e) { return e; } }, - fetchElasticFlamechart: async ({ - timeFrom, - timeTo, - kuery, - }: { - timeFrom: number; - timeTo: number; - kuery: string; - }) => { + fetchElasticFlamechart: async ({ http, timeFrom, timeTo, kuery }) => { try { const query: HttpFetchQuery = { timeFrom, timeTo, kuery, }; - const baseFlamegraph: BaseFlameGraph = await core.http.get(paths.Flamechart, { query }); + const baseFlamegraph = (await http.get(paths.Flamechart, { query })) as BaseFlameGraph; return createFlameGraph(baseFlamegraph); } catch (e) { return e; diff --git a/x-pack/plugins/profiling/server/routes/flamechart.ts b/x-pack/plugins/profiling/server/routes/flamechart.ts index 772d5058174846..49f0415049ad7d 100644 --- a/x-pack/plugins/profiling/server/routes/flamechart.ts +++ b/x-pack/plugins/profiling/server/routes/flamechart.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { RouteRegisterParameters } from '.'; import { getRoutePaths } from '../../common'; import { createCalleeTree } from '../../common/callee'; +import { handleRouteHandlerError } from '../utils/handle_route_error_handler'; import { createBaseFlameGraph } from '../../common/flamegraph'; import { createProfilingEsClient } from '../utils/create_profiling_es_client'; import { withProfilingSpan } from '../utils/with_profiling_span'; @@ -71,14 +72,8 @@ export function registerFlameChartSearchRoute({ router, logger }: RouteRegisterP logger.info('returning payload response to client'); return response.ok({ body: flamegraph }); - } catch (e) { - logger.error(e); - return response.customError({ - statusCode: e.statusCode ?? 500, - body: { - message: e.message, - }, - }); + } catch (error) { + return handleRouteHandlerError({ error, logger, response }); } } ); diff --git a/x-pack/plugins/profiling/server/routes/functions.ts b/x-pack/plugins/profiling/server/routes/functions.ts index adbbc59003376c..9d3eed222b9087 100644 --- a/x-pack/plugins/profiling/server/routes/functions.ts +++ b/x-pack/plugins/profiling/server/routes/functions.ts @@ -9,6 +9,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { RouteRegisterParameters } from '.'; import { getRoutePaths } from '../../common'; import { createTopNFunctions } from '../../common/functions'; +import { handleRouteHandlerError } from '../utils/handle_route_error_handler'; import { createProfilingEsClient } from '../utils/create_profiling_es_client'; import { withProfilingSpan } from '../utils/with_profiling_span'; import { getClient } from './compat'; @@ -72,14 +73,8 @@ export function registerTopNFunctionsSearchRoute({ router, logger }: RouteRegist return response.ok({ body: topNFunctions, }); - } catch (e) { - logger.error(e); - return response.customError({ - statusCode: e.statusCode ?? 500, - body: { - message: e.message, - }, - }); + } catch (error) { + return handleRouteHandlerError({ error, logger, response }); } } ); diff --git a/x-pack/plugins/profiling/server/routes/topn.ts b/x-pack/plugins/profiling/server/routes/topn.ts index 790f40049e1670..a8a7efc01bb522 100644 --- a/x-pack/plugins/profiling/server/routes/topn.ts +++ b/x-pack/plugins/profiling/server/routes/topn.ts @@ -14,6 +14,7 @@ import { computeBucketWidthFromTimeRangeAndBucketCount } from '../../common/hist import { groupStackFrameMetadataByStackTrace, StackTraceID } from '../../common/profiling'; import { getFieldNameForTopNType, TopNType } from '../../common/stack_traces'; import { createTopNSamples, getTopNAggregationRequest, TopNResponse } from '../../common/topn'; +import { handleRouteHandlerError } from '../utils/handle_route_error_handler'; import { ProfilingRequestHandlerContext } from '../types'; import { createProfilingEsClient, ProfilingESClient } from '../utils/create_profiling_es_client'; import { withProfilingSpan } from '../utils/with_profiling_span'; @@ -189,15 +190,8 @@ export function queryTopNCommon( kuery, }), }); - } catch (e) { - logger.error(e); - - return response.customError({ - statusCode: e.statusCode ?? 500, - body: { - message: 'Profiling TopN request failed: ' + e.message + '; full error ' + e.toString(), - }, - }); + } catch (error) { + return handleRouteHandlerError({ error, logger, response }); } } ); diff --git a/x-pack/plugins/profiling/server/utils/handle_route_error_handler.ts b/x-pack/plugins/profiling/server/utils/handle_route_error_handler.ts new file mode 100644 index 00000000000000..782beeeae15cb2 --- /dev/null +++ b/x-pack/plugins/profiling/server/utils/handle_route_error_handler.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KibanaResponseFactory } from '@kbn/core-http-server'; +import { Logger } from '@kbn/logging'; +import { WrappedElasticsearchClientError } from '@kbn/observability-plugin/server'; +import { errors } from '@elastic/elasticsearch'; + +export function handleRouteHandlerError({ + error, + logger, + response, +}: { + error: any; + response: KibanaResponseFactory; + logger: Logger; +}) { + if ( + error instanceof WrappedElasticsearchClientError && + error.originalError instanceof errors.RequestAbortedError + ) { + return response.custom({ + statusCode: 499, + body: { + message: 'Client closed request', + }, + }); + } + logger.error(error); + + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message, + }, + }); +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/single_ping_result.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/single_ping_result.tsx index 89fb255dc80efe..bb0666abbab26f 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/single_ping_result.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/single_ping_result.tsx @@ -24,7 +24,7 @@ export const SinglePingResult = ({ ping, loading }: { ping: Ping; loading: boole const responseStatus = !loading ? ping?.http?.response?.status_code : undefined; return ( - + IP {ip} {DURATION_LABEL} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_details_panel.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_details_panel.tsx index 3d8013c79beff2..2fd702a187e36f 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_details_panel.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_details_panel.tsx @@ -55,7 +55,7 @@ export const MonitorDetailsPanel = () => { return (
- + {ENABLED_LABEL} {monitor && ( diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/step_duration_panel.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/step_duration_panel.tsx index 671e661f4eb065..120449b88bd042 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/step_duration_panel.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/step_duration_panel.tsx @@ -12,6 +12,7 @@ import { ReportTypes } from '@kbn/observability-plugin/public'; import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; +import { useSelectedMonitor } from '../hooks/use_selected_monitor'; import { ClientPluginsStart } from '../../../../../plugin'; export const StepDurationPanel = () => { const { observability } = useKibana().services; @@ -20,12 +21,16 @@ export const StepDurationPanel = () => { const { monitorId } = useParams<{ monitorId: string }>(); + const { monitor } = useSelectedMonitor(); + + const isBrowser = monitor?.type === 'browser'; + return ( -

{DURATION_BY_STEP_LABEL}

+

{isBrowser ? DURATION_BY_STEP_LABEL : DURATION_BY_LOCATION}

@@ -43,10 +48,10 @@ export const StepDurationPanel = () => { { name: DURATION_BY_STEP_LABEL, reportDefinitions: { 'monitor.id': [monitorId] }, - selectedMetricField: 'synthetics.step.duration.us', + selectedMetricField: isBrowser ? 'synthetics.step.duration.us' : 'monitor.duration.us', dataType: 'synthetics', time: { from: 'now-24h/h', to: 'now' }, - breakdown: 'synthetics.step.name.keyword', + breakdown: isBrowser ? 'synthetics.step.name.keyword' : 'observer.geo.name', operationType: 'last_value', seriesType: 'area_stacked', }, @@ -60,6 +65,10 @@ const DURATION_BY_STEP_LABEL = i18n.translate('xpack.synthetics.detailsPanel.dur defaultMessage: 'Duration by step', }); +const DURATION_BY_LOCATION = i18n.translate('xpack.synthetics.detailsPanel.durationByLocation', { + defaultMessage: 'Duration by location', +}); + const LAST_24H_LABEL = i18n.translate('xpack.synthetics.detailsPanel.last24Hours', { defaultMessage: 'Last 24 hours', }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/api.ts index f2541b119e56d2..80713e587cefac 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/api.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/api.ts @@ -51,8 +51,8 @@ export const fetchSyntheticsMonitor = async ({ )) as SavedObject; return { - id: savedObject.id, ...savedObject.attributes, + id: savedObject.id, updated_at: savedObject.updated_at, } as EncryptedSyntheticsSavedMonitor; }; diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 9a47ad9de093ef..890e4c8a5e4f1f 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -4109,6 +4109,9 @@ "items": { "type": "keyword" } + }, + "total": { + "type": "long" } } }, diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/table/components/field_browser/field_browser.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/table/components/field_browser/field_browser.test.tsx index f8a0e9b9f99c6b..26b25ad88bd6e7 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/table/components/field_browser/field_browser.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/table/components/field_browser/field_browser.test.tsx @@ -36,7 +36,9 @@ describe('', () => { columnIds: [], onResetColumns: stub, onToggleColumn: stub, - options: {}, + options: { + preselectedCategoryIds: ['threat'], + }, }) ); }); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/table/components/field_browser/field_browser.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/table/components/field_browser/field_browser.tsx index fae02b89cefc37..8a5a95f04b4417 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/table/components/field_browser/field_browser.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/table/components/field_browser/field_browser.tsx @@ -29,6 +29,8 @@ export const IndicatorsFieldBrowser: VFC = ({ columnIds, onResetColumns, onToggleColumn, - options: {}, + options: { + preselectedCategoryIds: ['threat'], + }, }); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/field_browser.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/field_browser.test.tsx index ca5552c934be9b..5c3a33d5b17c85 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/field_browser.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/field_browser.test.tsx @@ -121,4 +121,25 @@ describe('FieldsBrowser', () => { const result = renderComponent({ isEventViewer, width: FIELD_BROWSER_WIDTH }); expect(result.getByTestId('show-field-browser')).toBeInTheDocument(); }); + + describe('options.preselectedCategoryIds', () => { + it("should render fields list narrowed to preselected category id's", async () => { + const agentFieldsCount = Object.keys(mockBrowserFields.agent?.fields || {}).length; + + // Narrowing the selection to 'agent' only + const result = renderComponent({ options: { preselectedCategoryIds: ['agent'] } }); + + result.getByTestId('show-field-browser').click(); + + // Wait for the modal to open + await waitFor(() => { + expect(result.getByTestId('fields-browser-container')).toBeInTheDocument(); + }); + + // Check if there are only 4 fields in the table + expect(result.queryByTestId('field-table')?.querySelectorAll('tbody tr')).toHaveLength( + agentFieldsCount + ); + }); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/field_browser.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/field_browser.tsx index 219a06713e7c38..32fe7a6f74df87 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/field_browser.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/field_browser.tsx @@ -31,6 +31,11 @@ export const FieldBrowserComponent: React.FC = ({ options, width, }) => { + const initialCategories = useMemo( + () => options?.preselectedCategoryIds ?? [], + [options?.preselectedCategoryIds] + ); + const customizeColumnsButtonRef = useRef(null); /** all field names shown in the field browser must contain this string (when specified) */ const [filterInput, setFilterInput] = useState(''); @@ -43,7 +48,7 @@ export const FieldBrowserComponent: React.FC = ({ /** when true, show a spinner in the input to indicate the field browser is searching for matching field names */ const [isSearching, setIsSearching] = useState(false); /** this category will be displayed in the right-hand pane of the field browser */ - const [selectedCategoryIds, setSelectedCategoryIds] = useState([]); + const [selectedCategoryIds, setSelectedCategoryIds] = useState(initialCategories); /** show the field browser */ const [show, setShow] = useState(false); @@ -93,9 +98,9 @@ export const FieldBrowserComponent: React.FC = ({ setFilteredBrowserFields(null); setFilterSelectedEnabled(false); setIsSearching(false); - setSelectedCategoryIds([]); + setSelectedCategoryIds(initialCategories); setShow(false); - }, []); + }, [initialCategories]); /** Invoked when the user types in the filter input */ const updateFilter = useCallback( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/types.ts index caed72a14b0c28..898fd671408378 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/field_browser/types.ts @@ -34,6 +34,10 @@ export type GetFieldTableColumns = (params: { export interface FieldBrowserOptions { createFieldButton?: CreateFieldComponent; getFieldTableColumns?: GetFieldTableColumns; + /** + * Categories that should be selected initially + */ + preselectedCategoryIds?: string[]; } export interface FieldBrowserProps {