diff --git a/packages/core/http/core-http-server/src/versioning/types.ts b/packages/core/http/core-http-server/src/versioning/types.ts index baa550b253544..b7aed68438fdb 100644 --- a/packages/core/http/core-http-server/src/versioning/types.ts +++ b/packages/core/http/core-http-server/src/versioning/types.ts @@ -32,7 +32,7 @@ export type VersionedRouteConfig = Omit< RouteConfig, 'validate' | 'options' > & { - options?: Omit, 'access'>; + options?: Omit, 'access' | 'description'>; /** See {@link RouteConfigOptions['access']} */ access: Exclude['access'], undefined>; /** diff --git a/packages/kbn-esql-utils/index.ts b/packages/kbn-esql-utils/index.ts index ded6e218ecb67..cdd512c2d92f3 100644 --- a/packages/kbn-esql-utils/index.ts +++ b/packages/kbn-esql-utils/index.ts @@ -18,7 +18,6 @@ export { appendToESQLQuery, appendWhereClauseToESQLQuery, TextBasedLanguages, - getESQLQueryColumns, } from './src'; export { ESQL_LATEST_VERSION } from './constants'; diff --git a/packages/kbn-esql-utils/src/index.ts b/packages/kbn-esql-utils/src/index.ts index d90377cd61ae8..adbfe6b23970a 100644 --- a/packages/kbn-esql-utils/src/index.ts +++ b/packages/kbn-esql-utils/src/index.ts @@ -17,4 +17,3 @@ export { removeDropCommandsFromESQLQuery, } from './utils/query_parsing_helpers'; export { appendToESQLQuery, appendWhereClauseToESQLQuery } from './utils/append_to_query'; -export { getESQLQueryColumns } from './utils/run_query_utils'; diff --git a/packages/kbn-esql-utils/src/utils/run_query_utils.ts b/packages/kbn-esql-utils/src/utils/run_query_utils.ts deleted file mode 100644 index 1512c86b2b8bf..0000000000000 --- a/packages/kbn-esql-utils/src/utils/run_query_utils.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -import type { DatatableColumn } from '@kbn/expressions-plugin/common'; -import type { ISearchStart } from '@kbn/data-plugin/public'; -import { esFieldTypeToKibanaFieldType } from '@kbn/field-types'; -import type { ESQLSearchReponse } from '@kbn/es-types'; -import { lastValueFrom } from 'rxjs'; -import { ESQL_LATEST_VERSION } from '../../constants'; - -export async function getESQLQueryColumns({ - esqlQuery, - search, - signal, -}: { - esqlQuery: string; - search: ISearchStart; - signal?: AbortSignal; -}): Promise { - const response = await lastValueFrom( - search.search( - { - params: { - query: `${esqlQuery} | limit 0`, - version: ESQL_LATEST_VERSION, - }, - }, - { - abortSignal: signal, - strategy: 'esql_async', - } - ) - ); - - const columns = - (response.rawResponse as unknown as ESQLSearchReponse).columns?.map(({ name, type }) => { - const kibanaType = esFieldTypeToKibanaFieldType(type); - const column = { - id: name, - name, - meta: { type: kibanaType, esType: type }, - } as DatatableColumn; - - return column; - }) ?? []; - - return columns; -} diff --git a/packages/kbn-esql-utils/tsconfig.json b/packages/kbn-esql-utils/tsconfig.json index 1185d03b56448..5a494e9929d7b 100644 --- a/packages/kbn-esql-utils/tsconfig.json +++ b/packages/kbn-esql-utils/tsconfig.json @@ -17,12 +17,8 @@ ], "kbn_references": [ "@kbn/data-views-plugin", - "@kbn/data-plugin", "@kbn/crypto-browser", "@kbn/data-view-utils", "@kbn/esql-ast", - "@kbn/expressions-plugin", - "@kbn/field-types", - "@kbn/es-types", ] } diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/fetch_historical_summary.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/fetch_historical_summary.ts index c60fe499fdec5..0df546beecc4c 100644 --- a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/fetch_historical_summary.ts +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/fetch_historical_summary.ts @@ -31,7 +31,13 @@ const fetchHistoricalSummaryParamsSchema = t.type({ groupBy: allOrAnyStringOrArray, revision: t.number, }), - t.partial({ remoteName: t.string }), + t.partial({ + remoteName: t.string, + range: t.type({ + from: t.string, + to: t.string, + }), + }), ]) ), }), diff --git a/x-pack/packages/kbn-slo-schema/src/schema/common.ts b/x-pack/packages/kbn-slo-schema/src/schema/common.ts index 155164dee593f..5346b9507e0b9 100644 --- a/x-pack/packages/kbn-slo-schema/src/schema/common.ts +++ b/x-pack/packages/kbn-slo-schema/src/schema/common.ts @@ -81,7 +81,10 @@ const groupSummarySchema = t.type({ noData: t.number, }); -const dateRangeSchema = t.type({ from: dateType, to: dateType }); +const dateRangeSchema = t.type({ + from: t.union([dateType, t.string]), + to: t.union([dateType, t.string]), +}); export { ALL_VALUE, diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/helpers.test.ts b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/helpers.test.ts index fcdb83a978f6a..57638b61db1a9 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/helpers.test.ts +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/helpers.test.ts @@ -5,7 +5,7 @@ * 2.0. */ import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; -import { getESQLQueryColumns } from '@kbn/esql-utils'; +import { fetchFieldsFromESQL } from '@kbn/text-based-editor'; import type { LensPluginStartDependencies } from '../../../plugin'; import { createMockStartDependencies } from '../../../editor_frame_service/mocks'; import { @@ -18,44 +18,45 @@ import { suggestionsApi } from '../../../lens_suggestions_api'; import { getSuggestions } from './helpers'; const mockSuggestionApi = suggestionsApi as jest.Mock; -const mockFetchData = getESQLQueryColumns as jest.Mock; +const mockFetchData = fetchFieldsFromESQL as jest.Mock; jest.mock('../../../lens_suggestions_api', () => ({ suggestionsApi: jest.fn(() => mockAllSuggestions), })); -jest.mock('@kbn/esql-utils', () => { - return { - getESQLQueryColumns: jest.fn().mockResolvedValue(() => [ - { - name: '@timestamp', - id: '@timestamp', - meta: { - type: 'date', +jest.mock('@kbn/text-based-editor', () => ({ + fetchFieldsFromESQL: jest.fn(() => { + return { + columns: [ + { + name: '@timestamp', + id: '@timestamp', + meta: { + type: 'date', + }, }, - }, - { - name: 'bytes', - id: 'bytes', - meta: { - type: 'number', + { + name: 'bytes', + id: 'bytes', + meta: { + type: 'number', + }, }, - }, - { - name: 'memory', - id: 'memory', - meta: { - type: 'number', + { + name: 'memory', + id: 'memory', + meta: { + type: 'number', + }, }, - }, - ]), - getIndexPatternFromESQLQuery: jest.fn().mockReturnValue('index1'), - }; -}); + ], + }; + }), +})); describe('getSuggestions', () => { const query = { - esql: 'from index1 | limit 10 | stats average = avg(bytes)', + esql: 'from index1 | limit 10 | stats average = avg(bytes', }; const mockStartDependencies = createMockStartDependencies() as unknown as LensPluginStartDependencies; diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/helpers.ts b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/helpers.ts index 9ab8b5fc05aef..803fcbf169935 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/helpers.ts +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/helpers.ts @@ -4,20 +4,39 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { - getIndexPatternFromSQLQuery, - getIndexPatternFromESQLQuery, - getESQLAdHocDataview, - getESQLQueryColumns, -} from '@kbn/esql-utils'; +import { getIndexPatternFromSQLQuery, getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; import type { AggregateQuery } from '@kbn/es-query'; +import { getESQLAdHocDataview } from '@kbn/esql-utils'; import { getLensAttributesFromSuggestion } from '@kbn/visualization-utils'; +import { fetchFieldsFromESQL } from '@kbn/text-based-editor'; import type { DataViewSpec } from '@kbn/data-views-plugin/public'; import type { TypedLensByValueInput } from '../../../embeddable/embeddable_component'; import type { LensPluginStartDependencies } from '../../../plugin'; import type { DatasourceMap, VisualizationMap } from '../../../types'; import { suggestionsApi } from '../../../lens_suggestions_api'; +export const getQueryColumns = async ( + query: AggregateQuery, + deps: LensPluginStartDependencies, + abortController?: AbortController +) => { + // Fetching only columns for ES|QL for performance reasons with limit 0 + // Important note: ES doesnt return the warnings for 0 limit, + // I am skipping them in favor of performance now + // but we should think another way to get them (from Lens embeddable or store) + const performantQuery = { ...query }; + if ('esql' in performantQuery && performantQuery.esql) { + performantQuery.esql = `${performantQuery.esql} | limit 0`; + } + const table = await fetchFieldsFromESQL( + performantQuery, + deps.expressions, + undefined, + abortController + ); + return table?.columns; +}; + export const getSuggestions = async ( query: AggregateQuery, deps: LensPluginStartDependencies, @@ -46,12 +65,7 @@ export const getSuggestions = async ( if (dataView.fields.getByName('@timestamp')?.type === 'date' && !dataViewSpec) { dataView.timeFieldName = '@timestamp'; } - - const columns = await getESQLQueryColumns({ - esqlQuery: 'esql' in query ? query.esql : '', - search: deps.data.search, - signal: abortController?.signal, - }); + const columns = await getQueryColumns(query, deps, abortController); const context = { dataViewSpec: dataView?.toSpec(), fieldName: '', diff --git a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx index eacbbc079413d..4513e0f4bffd4 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -55,7 +55,7 @@ export function createMockSetupDependencies() { export function createMockStartDependencies() { return { - data: dataPluginMock.createStartContract(), + data: dataPluginMock.createSetupContract(), embeddable: embeddablePluginMock.createStartContract(), expressions: expressionsPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts index 0d6f85e7703ad..3e77f8979a872 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts @@ -6,13 +6,13 @@ */ import { createGetterSetter } from '@kbn/kibana-utils-plugin/common'; import type { CoreStart } from '@kbn/core/public'; -import { getESQLQueryColumns } from '@kbn/esql-utils'; import { getLensAttributesFromSuggestion } from '@kbn/visualization-utils'; import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; import { PresentationContainer } from '@kbn/presentation-containers'; import { getESQLAdHocDataview, getIndexForESQLQuery } from '@kbn/esql-utils'; import type { Datasource, Visualization } from '../../types'; import type { LensPluginStartDependencies } from '../../plugin'; +import { fetchDataFromAggregateQuery } from '../../datasources/text_based/fetch_data_from_aggregate_query'; import { suggestionsApi } from '../../lens_suggestions_api'; import { generateId } from '../../id_generator'; import { executeEditAction } from './edit_action_helpers'; @@ -66,17 +66,21 @@ export async function executeCreateAction({ // so we are requesting them with limit 0 // this is much more performant than requesting // all the table - const abortController = new AbortController(); - const columns = await getESQLQueryColumns({ - esqlQuery: `from ${defaultIndex}`, - search: deps.data.search, - signal: abortController.signal, - }); + const performantQuery = { + esql: `from ${defaultIndex} | limit 0`, + }; + + const table = await fetchDataFromAggregateQuery( + performantQuery, + dataView, + deps.data, + deps.expressions + ); const context = { dataViewSpec: dataView.toSpec(), fieldName: '', - textBasedColumns: columns, + textBasedColumns: table?.columns, query: defaultEsqlQuery, }; diff --git a/x-pack/plugins/observability_solution/slo/common/locators/paths.ts b/x-pack/plugins/observability_solution/slo/common/locators/paths.ts index 2f2a1b329543b..198b433e285a2 100644 --- a/x-pack/plugins/observability_solution/slo/common/locators/paths.ts +++ b/x-pack/plugins/observability_solution/slo/common/locators/paths.ts @@ -4,11 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + export const SLOS_BASE_PATH = '/app/slos'; export const SLO_PREFIX = '/slos'; export const SLOS_PATH = '/' as const; export const SLOS_WELCOME_PATH = '/welcome' as const; -export const SLO_DETAIL_PATH = '/:sloId' as const; +export const SLO_DETAIL_PATH = '/:sloId/:tabId?' as const; export const SLO_CREATE_PATH = '/create' as const; export const SLO_EDIT_PATH = '/edit/:sloId' as const; export const SLOS_OUTDATED_DEFINITIONS_PATH = '/outdated-definitions' as const; @@ -25,10 +26,13 @@ export const paths = { sloEdit: (sloId: string) => `${SLOS_BASE_PATH}/edit/${encodeURIComponent(sloId)}`, sloEditWithEncodedForm: (sloId: string, encodedParams: string) => `${SLOS_BASE_PATH}/edit/${encodeURIComponent(sloId)}?_a=${encodedParams}`, - sloDetails: (sloId: string, instanceId?: string, remoteName?: string) => { + sloDetails: (sloId: string, instanceId?: string, remoteName?: string, tabId?: string) => { const qs = new URLSearchParams(); - if (!!instanceId) qs.append('instanceId', instanceId); + if (!!instanceId && instanceId !== '*') qs.append('instanceId', instanceId); if (!!remoteName) qs.append('remoteName', remoteName); + if (tabId) { + return `${SLOS_BASE_PATH}/${encodeURIComponent(sloId)}/${tabId}?${qs.toString()}`; + } return `${SLOS_BASE_PATH}/${encodeURIComponent(sloId)}?${qs.toString()}`; }, }; diff --git a/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/burn_rate_header.tsx b/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/burn_rate_header.tsx new file mode 100644 index 0000000000000..5df9bcb2255b4 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/burn_rate_header.tsx @@ -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 { EuiButtonGroup, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { SloTabId } from '../../../pages/slo_details/components/slo_details'; +import { BurnRateOption } from './burn_rates'; +interface Props { + burnRateOption: BurnRateOption; + setBurnRateOption: (option: BurnRateOption) => void; + burnRateOptions: BurnRateOption[]; + selectedTabId: SloTabId; +} +export function BurnRateHeader({ + burnRateOption, + burnRateOptions, + setBurnRateOption, + selectedTabId, +}: Props) { + const onBurnRateOptionChange = (optionId: string) => { + const selected = burnRateOptions.find((opt) => opt.id === optionId) ?? burnRateOptions[0]; + setBurnRateOption(selected); + }; + return ( + + + +

+ {i18n.translate('xpack.slo.burnRate.title', { + defaultMessage: 'Burn rate', + })} +

+
+
+ {selectedTabId !== 'history' && ( + + ({ id: opt.id, label: opt.label }))} + idSelected={burnRateOption.id} + onChange={onBurnRateOptionChange} + buttonSize="compressed" + /> + + )} +
+ ); +} diff --git a/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/burn_rates.tsx b/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/burn_rates.tsx index b6c828f9a4df6..cb7555dfeaec3 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/burn_rates.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/burn_rates.tsx @@ -5,11 +5,14 @@ * 2.0. */ -import { EuiButtonGroup, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import moment from 'moment'; import React, { useEffect, useState } from 'react'; +import { TimeBounds } from '../../../pages/slo_details/types'; +import { TimeRange } from '../error_rate_chart/use_lens_definition'; +import { SloTabId } from '../../../pages/slo_details/components/slo_details'; +import { BurnRateHeader } from './burn_rate_header'; import { useFetchSloBurnRates } from '../../../hooks/use_fetch_slo_burn_rates'; import { ErrorRateChart } from '../error_rate_chart'; import { BurnRate } from './burn_rate'; @@ -18,6 +21,9 @@ interface Props { slo: SLOWithSummaryResponse; isAutoRefreshing?: boolean; burnRateOptions: BurnRateOption[]; + selectedTabId: SloTabId; + range?: TimeRange; + onBrushed?: (timeBounds: TimeBounds) => void; } export interface BurnRateOption { @@ -32,7 +38,14 @@ function getWindowsFromOptions(opts: BurnRateOption[]): Array<{ name: string; du return opts.map((opt) => ({ name: opt.windowName, duration: `${opt.duration}h` })); } -export function BurnRates({ slo, isAutoRefreshing, burnRateOptions }: Props) { +export function BurnRates({ + slo, + isAutoRefreshing, + burnRateOptions, + selectedTabId, + range, + onBrushed, +}: Props) { const [burnRateOption, setBurnRateOption] = useState(burnRateOptions[0]); const { isLoading, data } = useFetchSloBurnRates({ slo, @@ -46,12 +59,7 @@ export function BurnRates({ slo, isAutoRefreshing, burnRateOptions }: Props) { } }, [burnRateOptions]); - const onBurnRateOptionChange = (optionId: string) => { - const selected = burnRateOptions.find((opt) => opt.id === optionId) ?? burnRateOptions[0]; - setBurnRateOption(selected); - }; - - const dataTimeRange = { + const dataTimeRange = range ?? { from: moment().subtract(burnRateOption.duration, 'hour').toDate(), to: new Date(), }; @@ -64,34 +72,26 @@ export function BurnRates({ slo, isAutoRefreshing, burnRateOptions }: Props) { return ( - - - -

- {i18n.translate('xpack.slo.burnRate.title', { - defaultMessage: 'Burn rate', - })} -

-
-
- - ({ id: opt.id, label: opt.label }))} - idSelected={burnRateOption.id} - onChange={onBurnRateOptionChange} - buttonSize="compressed" - /> - -
+ - - - + {selectedTabId !== 'history' && ( + + + + )} - +
diff --git a/x-pack/plugins/observability_solution/slo/public/components/slo/error_rate_chart/error_rate_chart.tsx b/x-pack/plugins/observability_solution/slo/public/components/slo/error_rate_chart/error_rate_chart.tsx index 9353d1d574718..e42f5a9115bb6 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/slo/error_rate_chart/error_rate_chart.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/slo/error_rate_chart/error_rate_chart.tsx @@ -9,6 +9,8 @@ import { ViewMode } from '@kbn/embeddable-plugin/public'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import moment from 'moment'; import React from 'react'; +import { SloTabId } from '../../../pages/slo_details/components/slo_details'; +import { TimeBounds } from '../../../pages/slo_details/types'; import { useKibana } from '../../../utils/kibana_react'; import { getDelayInSecondsFromSLO } from '../../../utils/slo/get_delay_in_seconds_from_slo'; import { AlertAnnotation, TimeRange, useLensDefinition } from './use_lens_definition'; @@ -20,6 +22,8 @@ interface Props { alertTimeRange?: TimeRange; showErrorRateAsLine?: boolean; annotations?: AlertAnnotation[]; + selectedTabId?: SloTabId; + onBrushed?: (timeBounds: TimeBounds) => void; } export function ErrorRateChart({ @@ -29,17 +33,20 @@ export function ErrorRateChart({ alertTimeRange, showErrorRateAsLine, annotations, + onBrushed, + selectedTabId, }: Props) { const { lens: { EmbeddableComponent }, } = useKibana().services; - const lensDef = useLensDefinition( + const lensDef = useLensDefinition({ slo, threshold, alertTimeRange, annotations, - showErrorRateAsLine - ); + showErrorRateAsLine, + selectedTabId, + }); const delayInSeconds = getDelayInSecondsFromSLO(slo); const from = moment(dataTimeRange.from).subtract(delayInSeconds, 'seconds').toISOString(); @@ -55,6 +62,14 @@ export function ErrorRateChart({ }} attributes={lensDef} viewMode={ViewMode.VIEW} + onBrushEnd={({ range }) => { + onBrushed?.({ + from: range[0], + to: range[1], + fromUtc: moment(range[0]).format(), + toUtc: moment(range[1]).format(), + }); + }} noPadding /> ); diff --git a/x-pack/plugins/observability_solution/slo/public/components/slo/error_rate_chart/use_lens_definition.ts b/x-pack/plugins/observability_solution/slo/public/components/slo/error_rate_chart/use_lens_definition.ts index a132076f8c2ed..513b26c6c2d18 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/slo/error_rate_chart/use_lens_definition.ts +++ b/x-pack/plugins/observability_solution/slo/public/components/slo/error_rate_chart/use_lens_definition.ts @@ -12,6 +12,7 @@ import { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; import moment from 'moment'; import { v4 as uuidv4 } from 'uuid'; +import { SloTabId } from '../../../pages/slo_details/components/slo_details'; import { SLO_DESTINATION_INDEX_PATTERN } from '../../../../common/constants'; export interface TimeRange { @@ -24,13 +25,21 @@ export interface AlertAnnotation { total: number; } -export function useLensDefinition( - slo: SLOWithSummaryResponse, - threshold: number, - alertTimeRange?: TimeRange, - annotations?: AlertAnnotation[], - showErrorRateAsLine?: boolean -): TypedLensByValueInput['attributes'] { +export function useLensDefinition({ + slo, + threshold, + alertTimeRange, + annotations, + showErrorRateAsLine, + selectedTabId, +}: { + slo: SLOWithSummaryResponse; + threshold: number; + alertTimeRange?: TimeRange; + annotations?: AlertAnnotation[]; + showErrorRateAsLine?: boolean; + selectedTabId?: SloTabId; +}): TypedLensByValueInput['attributes'] { const { euiTheme } = useEuiTheme(); const interval = 'auto'; @@ -87,20 +96,24 @@ export function useLensDefinition( }, ], }, - { - layerId: '34298f84-681e-4fa3-8107-d6facb32ed92', - layerType: 'referenceLine', - accessors: ['0a42b72b-cd5a-4d59-81ec-847d97c268e6'], - yConfig: [ - { - forAccessor: '0a42b72b-cd5a-4d59-81ec-847d97c268e6', - axisMode: 'left', - textVisibility: true, - color: euiTheme.colors.danger, - iconPosition: 'right', - }, - ], - }, + ...(selectedTabId !== 'history' + ? [ + { + layerId: '34298f84-681e-4fa3-8107-d6facb32ed92', + layerType: 'referenceLine', + accessors: ['0a42b72b-cd5a-4d59-81ec-847d97c268e6'], + yConfig: [ + { + forAccessor: '0a42b72b-cd5a-4d59-81ec-847d97c268e6', + axisMode: 'left', + textVisibility: true, + color: euiTheme.colors.danger, + iconPosition: 'right', + }, + ], + }, + ] + : []), ...(!!alertTimeRange ? [ { diff --git a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/common/slo_overview_details.tsx b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/common/slo_overview_details.tsx index 14100474e00b3..5bfc07f3562bd 100644 --- a/x-pack/plugins/observability_solution/slo/public/embeddable/slo/common/slo_overview_details.tsx +++ b/x-pack/plugins/observability_solution/slo/public/embeddable/slo/common/slo_overview_details.tsx @@ -79,9 +79,9 @@ export function SloOverviewDetails({ {tabs.map((tab, index) => ( {tab.label} diff --git a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_historical_summary.ts b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_historical_summary.ts index 0627b1b50c923..95abc489d69de 100644 --- a/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_historical_summary.ts +++ b/x-pack/plugins/observability_solution/slo/public/hooks/use_fetch_historical_summary.ts @@ -23,11 +23,16 @@ export interface UseFetchHistoricalSummaryResponse { export interface Params { sloList: SLOWithSummaryResponse[]; shouldRefetch?: boolean; + range?: { + from: string; + to: string; + }; } export function useFetchHistoricalSummary({ sloList = [], shouldRefetch, + range, }: Params): UseFetchHistoricalSummaryResponse { const { http } = useKibana().services; @@ -40,6 +45,7 @@ export function useFetchHistoricalSummary({ revision: slo.revision, objective: slo.objective, budgetingMethod: slo.budgetingMethod, + range, })); const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({ diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_chart.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_chart.tsx index 552198aec1fa1..5b4794a3e9bcb 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_chart.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_chart.tsx @@ -10,6 +10,8 @@ import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiStat } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; +import { TimeBounds } from '../types'; +import { SloTabId } from './slo_details'; import { useKibana } from '../../../utils/kibana_react'; import { toDuration, toMinutes } from '../../../utils/slo/duration'; import { ChartData } from '../../../typings/slo'; @@ -33,9 +35,11 @@ export interface Props { data: ChartData[]; isLoading: boolean; slo: SLOWithSummaryResponse; + selectedTabId?: SloTabId; + onBrushed?: (timeBounds: TimeBounds) => void; } -export function ErrorBudgetChart({ data, isLoading, slo }: Props) { +export function ErrorBudgetChart({ data, isLoading, slo, selectedTabId, onBrushed }: Props) { const { uiSettings } = useKibana().services; const percentFormat = uiSettings.get('format:percent:defaultPattern'); const isSloFailed = slo.summary.status === 'DEGRADING' || slo.summary.status === 'VIOLATED'; @@ -53,23 +57,12 @@ export function ErrorBudgetChart({ data, isLoading, slo }: Props) { } return ( <> - - - - - {errorBudgetTimeRemainingFormatted ? ( + {selectedTabId !== 'history' && ( + - ) : null} - + {errorBudgetTimeRemainingFormatted ? ( + + + + ) : null} + + )} diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_chart_panel.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_chart_panel.tsx index 11034718c9100..b8ffb2f30a79e 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_chart_panel.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_chart_panel.tsx @@ -14,6 +14,8 @@ import { i18n } from '@kbn/i18n'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import React, { useState, useCallback } from 'react'; import { SaveModalDashboardProps } from '@kbn/presentation-util-plugin/public'; +import { TimeBounds } from '../types'; +import { SloTabId } from './slo_details'; import { useKibana } from '../../../utils/kibana_react'; import { ChartData } from '../../../typings/slo'; import { ErrorBudgetChart } from './error_budget_chart'; @@ -24,9 +26,11 @@ export interface Props { data: ChartData[]; isLoading: boolean; slo: SLOWithSummaryResponse; + selectedTabId: SloTabId; + onBrushed?: (timeBounds: TimeBounds) => void; } -export function ErrorBudgetChartPanel({ data, isLoading, slo }: Props) { +export function ErrorBudgetChartPanel({ data, isLoading, slo, selectedTabId, onBrushed }: Props) { const [isMouseOver, setIsMouseOver] = useState(false); const [isDashboardAttachmentReady, setDashboardAttachmentReady] = useState(false); @@ -81,9 +85,16 @@ export function ErrorBudgetChartPanel({ data, isLoading, slo }: Props) { showTitle={true} isMouseOver={isMouseOver} setDashboardAttachmentReady={setDashboardAttachmentReady} + selectedTabId={selectedTabId} /> - +
{isDashboardAttachmentReady ? ( diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_header.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_header.tsx index b4cf6ae23cbb1..4a484d9df013f 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_header.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/error_budget_header.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { rollingTimeWindowTypeSchema, SLOWithSummaryResponse } from '@kbn/slo-schema'; +import { SloTabId } from './slo_details'; import { useKibana } from '../../../utils/kibana_react'; import { toDurationAdverbLabel, toDurationLabel } from '../../../utils/slo/labels'; @@ -19,6 +20,7 @@ interface Props { showTitle?: boolean; isMouseOver?: boolean; setDashboardAttachmentReady?: (value: boolean) => void; + selectedTabId?: SloTabId; } export function ErrorBudgetHeader({ @@ -26,6 +28,7 @@ export function ErrorBudgetHeader({ showTitle = true, isMouseOver, setDashboardAttachmentReady, + selectedTabId, }: Props) { const { executionContext } = useKibana().services; const executionContextName = executionContext.get().name; @@ -57,16 +60,18 @@ export function ErrorBudgetHeader({ )} - - - {rollingTimeWindowTypeSchema.is(slo.timeWindow.type) - ? i18n.translate('xpack.slo.sloDetails.errorBudgetChartPanel.duration', { - defaultMessage: 'Last {duration}', - values: { duration: toDurationLabel(slo.timeWindow.duration) }, - }) - : toDurationAdverbLabel(slo.timeWindow.duration)} - - + {selectedTabId !== 'history' && ( + + + {rollingTimeWindowTypeSchema.is(slo.timeWindow.type) + ? i18n.translate('xpack.slo.sloDetails.errorBudgetChartPanel.duration', { + defaultMessage: 'Last {duration}', + values: { duration: toDurationLabel(slo.timeWindow.duration) }, + }) + : toDurationAdverbLabel(slo.timeWindow.duration)} + + + )} ); } diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/events_chart_panel.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/events_chart_panel.tsx index e2f85a9d47e68..340d6ce465884 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/events_chart_panel.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/events_chart_panel.tsx @@ -36,6 +36,9 @@ import { max, min } from 'lodash'; import moment from 'moment'; import React, { useRef } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; +import { TimeBounds } from '../types'; +import { getBrushData } from '../../../utils/slo/duration'; +import { SloTabId } from './slo_details'; import { useGetPreviewData } from '../../../hooks/use_get_preview_data'; import { useKibana } from '../../../utils/kibana_react'; import { COMPARATOR_MAPPING } from '../../slo_edit/constants'; @@ -48,9 +51,11 @@ export interface Props { start: number; end: number; }; + selectedTabId: SloTabId; + onBrushed?: (timeBounds: TimeBounds) => void; } -export function EventsChartPanel({ slo, range }: Props) { +export function EventsChartPanel({ slo, range, selectedTabId, onBrushed }: Props) { const { charts, uiSettings, discover } = useKibana().services; const { euiTheme } = useEuiTheme(); const baseTheme = charts.theme.useChartsBaseTheme(); @@ -157,13 +162,15 @@ export function EventsChartPanel({ slo, range }: Props) { {title} - - - {i18n.translate('xpack.slo.sloDetails.eventsChartPanel.duration', { - defaultMessage: 'Last 24h', - })} - - + {selectedTabId !== 'history' && ( + + + {i18n.translate('xpack.slo.sloDetails.eventsChartPanel.duration', { + defaultMessage: 'Last 24h', + })} + + + )} {showViewEventsLink && ( @@ -193,6 +200,7 @@ export function EventsChartPanel({ slo, range }: Props) { data={data || []} annotation={annotation} slo={slo} + onBrushed={onBrushed} /> ) : ( <> @@ -225,6 +233,9 @@ export function EventsChartPanel({ slo, range }: Props) { pointerUpdateDebounce={0} pointerUpdateTrigger={'x'} locale={i18n.getLocale()} + onBrushEnd={(brushArea) => { + onBrushed?.(getBrushData(brushArea)); + }} /> {annotation} diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/historical_data_charts.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/historical_data_charts.tsx new file mode 100644 index 0000000000000..f2e096195f474 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/historical_data_charts.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; +import { EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import { TimeBounds } from '../types'; +import { useFetchHistoricalSummary } from '../../../hooks/use_fetch_historical_summary'; +import { formatHistoricalData } from '../../../utils/slo/chart_data_formatter'; +import { SloTabId } from './slo_details'; +import { SliChartPanel } from './sli_chart_panel'; +import { ErrorBudgetChartPanel } from './error_budget_chart_panel'; + +export interface Props { + slo: SLOWithSummaryResponse; + isAutoRefreshing: boolean; + selectedTabId: SloTabId; + range?: { + from: string; + to: string; + }; + onBrushed?: (timeBounds: TimeBounds) => void; +} +export function HistoricalDataCharts({ + slo, + range, + isAutoRefreshing, + selectedTabId, + onBrushed, +}: Props) { + const { data: historicalSummaries = [], isLoading: historicalSummaryLoading } = + useFetchHistoricalSummary({ + sloList: [slo], + shouldRefetch: isAutoRefreshing, + range, + }); + + const sloHistoricalSummary = historicalSummaries.find( + (historicalSummary) => + historicalSummary.sloId === slo.id && + historicalSummary.instanceId === (slo.instanceId ?? ALL_VALUE) + ); + + const errorBudgetBurnDownData = formatHistoricalData( + sloHistoricalSummary?.data, + 'error_budget_remaining' + ); + const historicalSliData = formatHistoricalData(sloHistoricalSummary?.data, 'sli_value'); + + return ( + <> + + + + + + + + ); +} diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/history/slo_details_history.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/history/slo_details_history.tsx new file mode 100644 index 0000000000000..0e20cb3960fbc --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/history/slo_details_history.tsx @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useCallback, useMemo, useState } from 'react'; +import { SLOWithSummaryResponse } from '@kbn/slo-schema'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSuperDatePicker, + OnTimeChangeProps, + OnRefreshProps, + EuiSpacer, +} from '@elastic/eui'; +import DateMath from '@kbn/datemath'; +import { useKibana } from '../../../../utils/kibana_react'; +import { HistoricalDataCharts } from '../historical_data_charts'; +import { useBurnRateOptions } from '../../hooks/use_burn_rate_options'; +import { SloTabId } from '../slo_details'; +import { BurnRates } from '../../../../components/slo/burn_rate/burn_rates'; +import { EventsChartPanel } from '../events_chart_panel'; +export interface Props { + slo: SLOWithSummaryResponse; + isAutoRefreshing: boolean; + selectedTabId: SloTabId; +} + +export function SLODetailsHistory({ slo, isAutoRefreshing, selectedTabId }: Props) { + const { uiSettings } = useKibana().services; + + const { burnRateOptions } = useBurnRateOptions(slo); + + const [start, setStart] = useState('now-30d'); + const [end, setEnd] = useState('now'); + + const onTimeChange = (val: OnTimeChangeProps) => { + setStart(val.start); + setEnd(val.end); + }; + + const onRefresh = (val: OnRefreshProps) => {}; + + const absRange = useMemo(() => { + return { + from: new Date(DateMath.parse(start)!.valueOf()), + to: new Date(DateMath.parse(end, { roundUp: true })!.valueOf()), + absoluteFrom: DateMath.parse(start)!.valueOf(), + absoluteTo: DateMath.parse(end, { roundUp: true })!.valueOf(), + }; + }, [start, end]); + + const onBrushed = useCallback(({ fromUtc, toUtc }) => { + setStart(fromUtc); + setEnd(toUtc); + }, []); + + return ( + <> + + + { + return { + start: from, + end: to, + label: display, + }; + })} + /> + + + + + + + + + + + + + + + + ); +} diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/sli_chart_panel.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/sli_chart_panel.tsx index 788303b43ffa1..b4b90ae4efab1 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/sli_chart_panel.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/sli_chart_panel.tsx @@ -10,6 +10,8 @@ import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; import { rollingTimeWindowTypeSchema, SLOWithSummaryResponse } from '@kbn/slo-schema'; import React from 'react'; +import { TimeBounds } from '../types'; +import { SloTabId } from './slo_details'; import { useKibana } from '../../../utils/kibana_react'; import { ChartData } from '../../../typings/slo'; import { toDurationAdverbLabel, toDurationLabel } from '../../../utils/slo/labels'; @@ -19,9 +21,11 @@ export interface Props { data: ChartData[]; isLoading: boolean; slo: SLOWithSummaryResponse; + selectedTabId: SloTabId; + onBrushed?: (timeBounds: TimeBounds) => void; } -export function SliChartPanel({ data, isLoading, slo }: Props) { +export function SliChartPanel({ data, isLoading, slo, selectedTabId, onBrushed }: Props) { const { uiSettings } = useKibana().services; const percentFormat = uiSettings.get('format:percent:defaultPattern'); @@ -41,41 +45,45 @@ export function SliChartPanel({ data, isLoading, slo }: Props) { - - - {rollingTimeWindowTypeSchema.is(slo.timeWindow.type) - ? i18n.translate('xpack.slo.sloDetails.sliHistoryChartPanel.duration', { - defaultMessage: 'Last {duration}', - values: { duration: toDurationLabel(slo.timeWindow.duration) }, - }) - : toDurationAdverbLabel(slo.timeWindow.duration)} - - + {selectedTabId !== 'history' && ( + + + {rollingTimeWindowTypeSchema.is(slo.timeWindow.type) + ? i18n.translate('xpack.slo.sloDetails.sliHistoryChartPanel.duration', { + defaultMessage: 'Last {duration}', + values: { duration: toDurationLabel(slo.timeWindow.duration) }, + }) + : toDurationAdverbLabel(slo.timeWindow.duration)} + + + )} - - - - - - - - + {selectedTabId !== 'history' && ( + + + + + + + + + )} diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_details.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_details.tsx index 01d62098e8802..cb5a1420dc06a 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_details.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/slo_details.tsx @@ -5,71 +5,26 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, htmlIdGenerator } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import React, { useEffect, useState } from 'react'; -import { BurnRateOption, BurnRates } from '../../../components/slo/burn_rate/burn_rates'; -import { useFetchHistoricalSummary } from '../../../hooks/use_fetch_historical_summary'; -import { useFetchRulesForSlo } from '../../../hooks/use_fetch_rules_for_slo'; -import { formatHistoricalData } from '../../../utils/slo/chart_data_formatter'; -import { ErrorBudgetChartPanel } from './error_budget_chart_panel'; +import { HistoricalDataCharts } from './historical_data_charts'; +import { useBurnRateOptions } from '../hooks/use_burn_rate_options'; +import { SLODetailsHistory } from './history/slo_details_history'; +import { BurnRates } from '../../../components/slo/burn_rate/burn_rates'; import { EventsChartPanel } from './events_chart_panel'; import { Overview } from './overview/overview'; -import { SliChartPanel } from './sli_chart_panel'; import { SloDetailsAlerts } from './slo_detail_alerts'; import { SloHealthCallout } from './slo_health_callout'; import { SloRemoteCallout } from './slo_remote_callout'; export const TAB_ID_URL_PARAM = 'tabId'; export const OVERVIEW_TAB_ID = 'overview'; +export const HISTORY_TAB_ID = 'history'; export const ALERTS_TAB_ID = 'alerts'; const DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000; -const DEFAULT_BURN_RATE_OPTIONS: BurnRateOption[] = [ - { - id: htmlIdGenerator()(), - label: i18n.translate('xpack.slo.burnRates.fromRange.label', { - defaultMessage: '{duration}h', - values: { duration: 1 }, - }), - windowName: 'CRITICAL', - threshold: 14.4, - duration: 1, - }, - { - id: htmlIdGenerator()(), - label: i18n.translate('xpack.slo.burnRates.fromRange.label', { - defaultMessage: '{duration}h', - values: { duration: 6 }, - }), - windowName: 'HIGH', - threshold: 6, - duration: 6, - }, - { - id: htmlIdGenerator()(), - label: i18n.translate('xpack.slo.burnRates.fromRange.label', { - defaultMessage: '{duration}h', - values: { duration: 24 }, - }), - windowName: 'MEDIUM', - threshold: 3, - duration: 24, - }, - { - id: htmlIdGenerator()(), - label: i18n.translate('xpack.slo.burnRates.fromRange.label', { - defaultMessage: '{duration}h', - values: { duration: 72 }, - }), - windowName: 'LOW', - threshold: 1, - duration: 72, - }, -]; - -export type SloTabId = typeof OVERVIEW_TAB_ID | typeof ALERTS_TAB_ID; +export type SloTabId = typeof OVERVIEW_TAB_ID | typeof ALERTS_TAB_ID | typeof HISTORY_TAB_ID; export interface Props { slo: SLOWithSummaryResponse; @@ -77,30 +32,7 @@ export interface Props { selectedTabId: SloTabId; } export function SloDetails({ slo, isAutoRefreshing, selectedTabId }: Props) { - const { data: rules } = useFetchRulesForSlo({ sloIds: [slo.id] }); - const burnRateOptions = - rules?.[slo.id]?.[0]?.params?.windows?.map((window) => ({ - id: htmlIdGenerator()(), - label: i18n.translate('xpack.slo.burnRates.fromRange.label', { - defaultMessage: '{duration}h', - values: { duration: window.longWindow.value }, - }), - windowName: window.actionGroup, - threshold: window.burnRateThreshold, - duration: window.longWindow.value, - })) ?? DEFAULT_BURN_RATE_OPTIONS; - - const { data: historicalSummaries = [], isLoading: historicalSummaryLoading } = - useFetchHistoricalSummary({ - sloList: [slo], - shouldRefetch: isAutoRefreshing, - }); - - const sloHistoricalSummary = historicalSummaries.find( - (historicalSummary) => - historicalSummary.sloId === slo.id && - historicalSummary.instanceId === (slo.instanceId ?? ALL_VALUE) - ); + const { burnRateOptions } = useBurnRateOptions(slo); const [range, setRange] = useState({ start: new Date().getTime() - DAY_IN_MILLISECONDS, @@ -118,12 +50,6 @@ export function SloDetails({ slo, isAutoRefreshing, selectedTabId }: Props) { return () => clearInterval(intervalId); }, [isAutoRefreshing]); - const errorBudgetBurnDownData = formatHistoricalData( - sloHistoricalSummary?.data, - 'error_budget_remaining' - ); - const historicalSliData = formatHistoricalData(sloHistoricalSummary?.data, 'sli_value'); - return selectedTabId === OVERVIEW_TAB_ID ? ( @@ -137,24 +63,26 @@ export function SloDetails({ slo, isAutoRefreshing, selectedTabId }: Props) { slo={slo} isAutoRefreshing={isAutoRefreshing} burnRateOptions={burnRateOptions} + selectedTabId={selectedTabId} /> + - - - - - - - + - ) : ( + ) : selectedTabId === ALERTS_TAB_ID ? ( + ) : ( + ); } diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/wide_chart.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/wide_chart.tsx index d737590b190a5..5d6d68ae7d892 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/wide_chart.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/components/wide_chart.tsx @@ -24,6 +24,8 @@ import moment from 'moment'; import React, { useRef } from 'react'; import { i18n } from '@kbn/i18n'; +import { getBrushData } from '../../../utils/slo/duration'; +import { TimeBounds } from '../types'; import { useKibana } from '../../../utils/kibana_react'; import { ChartData } from '../../../typings'; @@ -36,9 +38,10 @@ export interface Props { chart: ChartType; state: State; isLoading: boolean; + onBrushed?: (timeBounds: TimeBounds) => void; } -export function WideChart({ chart, data, id, isLoading, state }: Props) { +export function WideChart({ chart, data, id, isLoading, state, onBrushed }: Props) { const { charts, uiSettings } = useKibana().services; const baseTheme = charts.theme.useChartsBaseTheme(); const { euiTheme } = useEuiTheme(); @@ -63,7 +66,16 @@ export function WideChart({ chart, data, id, isLoading, state }: Props) { } + noResults={ + + } onPointerUpdate={handleCursorUpdate} externalPointerEvents={{ tooltip: { visible: true }, @@ -71,6 +83,9 @@ export function WideChart({ chart, data, id, isLoading, state }: Props) { pointerUpdateDebounce={0} pointerUpdateTrigger={'x'} locale={i18n.getLocale()} + onBrushEnd={(brushArea) => { + onBrushed?.(getBrushData(brushArea)); + }} /> { + const { data: rules } = useFetchRulesForSlo({ sloIds: [slo.id] }); + const burnRateOptions = + rules?.[slo.id]?.[0]?.params?.windows?.map((window) => ({ + id: htmlIdGenerator()(), + label: i18n.translate('xpack.slo.burnRates.fromRange.label', { + defaultMessage: '{duration}h', + values: { duration: window.longWindow.value }, + }), + windowName: window.actionGroup, + threshold: window.burnRateThreshold, + duration: window.longWindow.value, + })) ?? DEFAULT_BURN_RATE_OPTIONS; + + return { burnRateOptions }; +}; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_selected_tab.ts b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_selected_tab.ts new file mode 100644 index 0000000000000..1b1ff616e9c09 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_selected_tab.ts @@ -0,0 +1,38 @@ +/* + * 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 { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { SloDetailsPathParams } from '../types'; +import { + ALERTS_TAB_ID, + HISTORY_TAB_ID, + OVERVIEW_TAB_ID, + SloTabId, +} from '../components/slo_details'; + +export const useSelectedTab = () => { + const { tabId } = useParams(); + + const [selectedTabId, setSelectedTabId] = useState(() => { + return tabId && [OVERVIEW_TAB_ID, ALERTS_TAB_ID, HISTORY_TAB_ID].includes(tabId) + ? (tabId as SloTabId) + : OVERVIEW_TAB_ID; + }); + + useEffect(() => { + // update the url when the selected tab changes + if (tabId !== selectedTabId) { + setSelectedTabId(tabId as SloTabId); + } + }, [selectedTabId, tabId]); + + return { + selectedTabId: selectedTabId || OVERVIEW_TAB_ID, + setSelectedTabId, + }; +}; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_details_tabs.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_details_tabs.tsx index 5926d4f2cf406..8eb954d4b5771 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_details_tabs.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/hooks/use_slo_details_tabs.tsx @@ -9,8 +9,15 @@ import { i18n } from '@kbn/i18n'; import { EuiNotificationBadge, EuiToolTip } from '@elastic/eui'; import React from 'react'; import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; +import { paths } from '../../../../common/locators/paths'; +import { useKibana } from '../../../utils/kibana_react'; import { useFetchActiveAlerts } from '../../../hooks/use_fetch_active_alerts'; -import { ALERTS_TAB_ID, OVERVIEW_TAB_ID, SloTabId } from '../components/slo_details'; +import { + ALERTS_TAB_ID, + HISTORY_TAB_ID, + OVERVIEW_TAB_ID, + SloTabId, +} from '../components/slo_details'; export const useSloDetailsTabs = ({ slo, @@ -21,13 +28,15 @@ export const useSloDetailsTabs = ({ slo?: SLOWithSummaryResponse | null; isAutoRefreshing: boolean; selectedTabId: SloTabId; - setSelectedTabId: (val: SloTabId) => void; + setSelectedTabId?: (val: SloTabId) => void; }) => { const { data: activeAlerts } = useFetchActiveAlerts({ sloIdsAndInstanceIds: slo ? [[slo.id, slo.instanceId ?? ALL_VALUE]] : [], shouldRefetch: isAutoRefreshing, }); + const { basePath } = useKibana().services.http; + const isRemote = !!slo?.remote; const tabs = [ @@ -38,8 +47,47 @@ export const useSloDetailsTabs = ({ }), 'data-test-subj': 'overviewTab', isSelected: selectedTabId === OVERVIEW_TAB_ID, - onClick: () => setSelectedTabId(OVERVIEW_TAB_ID), + ...(setSelectedTabId + ? { + onClick: () => setSelectedTabId(OVERVIEW_TAB_ID), + } + : { + href: slo + ? `${basePath.get()}${paths.sloDetails( + slo.id, + slo.instanceId, + slo.remote?.remoteName, + OVERVIEW_TAB_ID + )}` + : undefined, + }), }, + ...(slo?.timeWindow.type === 'rolling' + ? [ + { + id: HISTORY_TAB_ID, + label: i18n.translate('xpack.slo.sloDetails.tab.historyLabel', { + defaultMessage: 'History', + }), + 'data-test-subj': 'historyTab', + isSelected: selectedTabId === HISTORY_TAB_ID, + ...(setSelectedTabId + ? { + onClick: () => setSelectedTabId(HISTORY_TAB_ID), + } + : { + href: slo + ? `${basePath.get()}${paths.sloDetails( + slo.id, + slo.instanceId, + slo.remote?.remoteName, + HISTORY_TAB_ID + )}` + : undefined, + }), + }, + ] + : []), { id: ALERTS_TAB_ID, label: isRemote ? ( @@ -63,7 +111,20 @@ export const useSloDetailsTabs = ({ {(activeAlerts && activeAlerts.get(slo)) ?? 0} ) : null, - onClick: () => setSelectedTabId(ALERTS_TAB_ID), + ...(setSelectedTabId + ? { + onClick: () => setSelectedTabId(ALERTS_TAB_ID), + } + : { + href: slo + ? `${basePath.get()}${paths.sloDetails( + slo.id, + slo.instanceId, + slo.remote?.remoteName, + ALERTS_TAB_ID + )}` + : undefined, + }), }, ]; diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.test.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.test.tsx index cb8c8b37868d8..dd0b22e177701 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.test.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.test.tsx @@ -79,6 +79,7 @@ const mockKibana = () => { http: { basePath: { prepend: (url: string) => url, + get: () => 'http://localhost:5601', }, }, dataViews: { diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.tsx index 1fddae776ec01..3665bd68629f4 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/slo_details.tsx @@ -6,7 +6,7 @@ */ import React, { useEffect, useState } from 'react'; -import { useLocation, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { useIsMutating } from '@tanstack/react-query'; import { EuiLoadingSpinner, EuiSkeletonText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -16,6 +16,7 @@ import type { SLOWithSummaryResponse } from '@kbn/slo-schema'; import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public'; import dedent from 'dedent'; +import { useSelectedTab } from './hooks/use_selected_tab'; import { HeaderMenu } from '../../components/header_menu/header_menu'; import { useSloDetailsTabs } from './hooks/use_slo_details_tabs'; import { useKibana } from '../../utils/kibana_react'; @@ -23,13 +24,7 @@ import { usePluginContext } from '../../hooks/use_plugin_context'; import { useFetchSloDetails } from '../../hooks/use_fetch_slo_details'; import { useLicense } from '../../hooks/use_license'; import PageNotFound from '../404'; -import { - ALERTS_TAB_ID, - OVERVIEW_TAB_ID, - SloDetails, - TAB_ID_URL_PARAM, - SloTabId, -} from './components/slo_details'; +import { SloDetails } from './components/slo_details'; import { HeaderTitle } from './components/header_title'; import { HeaderControl } from './components/header_control'; import { paths } from '../../../common/locators/paths'; @@ -45,7 +40,6 @@ export function SloDetailsPage() { observabilityAIAssistant, } = useKibana().services; const { ObservabilityPageTemplate } = usePluginContext(); - const { search } = useLocation(); const { hasAtLeast } = useLicense(); const hasRightLicense = hasAtLeast('platinum'); @@ -61,19 +55,12 @@ export function SloDetailsPage() { }); const isDeleting = Boolean(useIsMutating(['deleteSlo'])); - const [selectedTabId, setSelectedTabId] = useState(() => { - const searchParams = new URLSearchParams(search); - const urlTabId = searchParams.get(TAB_ID_URL_PARAM); - return urlTabId && [OVERVIEW_TAB_ID, ALERTS_TAB_ID].includes(urlTabId) - ? (urlTabId as SloTabId) - : OVERVIEW_TAB_ID; - }); + const { selectedTabId } = useSelectedTab(); const { tabs } = useSloDetailsTabs({ slo, isAutoRefreshing, selectedTabId, - setSelectedTabId, }); useBreadcrumbs(getBreadcrumbs(basePath, slo)); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/types.ts b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/types.ts index 46aaa1b02f38b..7117cdd39db2c 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slo_details/types.ts +++ b/x-pack/plugins/observability_solution/slo/public/pages/slo_details/types.ts @@ -7,4 +7,12 @@ export interface SloDetailsPathParams { sloId: string; + tabId?: string; +} + +export interface TimeBounds { + from: number; + to: number; + fromUtc: string; + toUtc: string; } diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/good_bad_events_chart.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/good_bad_events_chart.tsx index f4d2b7aa2ceee..3f0e88b912ec2 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/good_bad_events_chart.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/common/good_bad_events_chart.tsx @@ -23,6 +23,8 @@ import { i18n } from '@kbn/i18n'; import { GetPreviewDataResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; import moment from 'moment'; import React, { useRef } from 'react'; +import { TimeBounds } from '../../../slo_details/types'; +import { getBrushData } from '../../../../utils/slo/duration'; import { useKibana } from '../../../../utils/kibana_react'; import { openInDiscover } from '../../../../utils/slo/get_discover_link'; @@ -32,6 +34,7 @@ export interface Props { annotation?: React.ReactNode; isLoading?: boolean; bottomTitle?: string; + onBrushed?: (timeBounds: TimeBounds) => void; } export function GoodBadEventsChart({ @@ -39,6 +42,7 @@ export function GoodBadEventsChart({ bottomTitle, data, slo, + onBrushed, isLoading = false, }: Props) { const { charts, uiSettings, discover } = useKibana().services; @@ -97,7 +101,16 @@ export function GoodBadEventsChart({ showLegend={true} showLegendExtra={false} legendPosition={Position.Left} - noResults={} + noResults={ + + } onPointerUpdate={handleCursorUpdate} externalPointerEvents={{ tooltip: { visible: true }, @@ -106,6 +119,9 @@ export function GoodBadEventsChart({ pointerUpdateTrigger={'x'} locale={i18n.getLocale()} onElementClick={barClickHandler as ElementClickListener} + onBrushEnd={(brushArea) => { + onBrushed?.(getBrushData(brushArea)); + }} /> {annotation} { const dateRangeBySlo = params.list.reduce>( - (acc, { sloId, timeWindow }) => { - acc[sloId] = getDateRange(timeWindow); + (acc, { sloId, timeWindow, range }) => { + acc[sloId] = range ?? getDateRange(timeWindow); return acc; }, {} @@ -272,6 +272,13 @@ function handleResultForRollingAndTimeslices( }); } +export const getEsDateRange = (dateRange: DateRange) => { + return { + gte: typeof dateRange.from === 'string' ? dateRange.from : dateRange.from.toISOString(), + lte: typeof dateRange.to === 'string' ? dateRange.to : dateRange.to.toISOString(), + }; +}; + function generateSearchQuery({ sloId, groupBy, @@ -309,10 +316,7 @@ function generateSearchQuery({ { term: { 'slo.revision': revision } }, { range: { - '@timestamp': { - gte: dateRange.from.toISOString(), - lte: dateRange.to.toISOString(), - }, + '@timestamp': getEsDateRange(dateRange), }, }, ...extraFilterByInstanceId, @@ -325,7 +329,7 @@ function generateSearchQuery({ field: '@timestamp', fixed_interval: fixedInterval, extended_bounds: { - min: dateRange.from.toISOString(), + min: typeof dateRange.from === 'string' ? dateRange.from : dateRange.from.toISOString(), max: 'now/d', }, }, diff --git a/x-pack/plugins/observability_solution/slo/server/services/summary_client.test.ts b/x-pack/plugins/observability_solution/slo/server/services/summary_client.test.ts index ec735058b1bd3..584aba75e22ff 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/summary_client.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/summary_client.test.ts @@ -58,7 +58,7 @@ describe('SummaryClient', () => { { term: { 'slo.revision': slo.revision } }, { range: { - '@timestamp': { gte: expect.anything(), lt: expect.anything() }, + '@timestamp': { gte: expect.anything(), lte: expect.anything() }, }, }, ], @@ -94,7 +94,7 @@ describe('SummaryClient', () => { range: { '@timestamp': { gte: expect.anything(), - lt: expect.anything(), + lte: expect.anything(), }, }, }, @@ -136,7 +136,7 @@ describe('SummaryClient', () => { { term: { 'slo.revision': slo.revision } }, { range: { - '@timestamp': { gte: expect.anything(), lt: expect.anything() }, + '@timestamp': { gte: expect.anything(), lte: expect.anything() }, }, }, ], @@ -188,7 +188,7 @@ describe('SummaryClient', () => { range: { '@timestamp': { gte: expect.anything(), - lt: expect.anything(), + lte: expect.anything(), }, }, }, diff --git a/x-pack/plugins/observability_solution/slo/server/services/summary_client.ts b/x-pack/plugins/observability_solution/slo/server/services/summary_client.ts index 4a41db114c9cf..5e5ee5a7228a0 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/summary_client.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/summary_client.ts @@ -16,6 +16,7 @@ import { occurrencesBudgetingMethodSchema, timeslicesBudgetingMethodSchema, } from '@kbn/slo-schema'; +import { getEsDateRange } from './historical_summary_client'; import { SLO_DESTINATION_INDEX_PATTERN } from '../../common/constants'; import { Groupings, Meta, SLODefinition, Summary } from '../domain/models'; import { computeSLI, computeSummaryStatus, toErrorBudget } from '../domain/services'; @@ -75,7 +76,7 @@ export class DefaultSummaryClient implements SummaryClient { { term: { 'slo.revision': slo.revision } }, { range: { - '@timestamp': { gte: dateRange.from.toISOString(), lt: dateRange.to.toISOString() }, + '@timestamp': getEsDateRange(dateRange), }, }, ...instanceIdFilter, diff --git a/x-pack/plugins/observability_solution/slo/tsconfig.json b/x-pack/plugins/observability_solution/slo/tsconfig.json index b8a9c1d5e21b2..70ff3d26f591f 100644 --- a/x-pack/plugins/observability_solution/slo/tsconfig.json +++ b/x-pack/plugins/observability_solution/slo/tsconfig.json @@ -97,6 +97,7 @@ "@kbn/data-view-field-editor-plugin", "@kbn/securitysolution-io-ts-utils", "@kbn/core-elasticsearch-server-mocks", + "@kbn/datemath", "@kbn/presentation-containers", ] } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/logic/esql_validator.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/logic/esql_validator.test.ts index 162fe94ecad70..889d74a1c6503 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/logic/esql_validator.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/logic/esql_validator.test.ts @@ -17,6 +17,19 @@ describe('computeHasMetadataOperator', () => { expect(computeHasMetadataOperator('from test* | eval x="[metadata _id]"')).toBe(false); }); it('should be true if query has operator', () => { + expect(computeHasMetadataOperator('from test* metadata _id')).toBe(true); + expect(computeHasMetadataOperator('from test* metadata _id, _index')).toBe(true); + expect(computeHasMetadataOperator('from test* metadata _index, _id')).toBe(true); + expect(computeHasMetadataOperator('from test* metadata _id ')).toBe(true); + expect(computeHasMetadataOperator('from test* metadata _id | limit 10')).toBe(true); + expect( + computeHasMetadataOperator(`from packetbeat* metadata + + _id + | limit 100`) + ).toBe(true); + + // still validates deprecated square bracket syntax expect(computeHasMetadataOperator('from test* [metadata _id]')).toBe(true); expect(computeHasMetadataOperator('from test* [metadata _id, _index]')).toBe(true); expect(computeHasMetadataOperator('from test* [metadata _index, _id]')).toBe(true); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/logic/esql_validator.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/logic/esql_validator.ts index 9cadae108ac22..e7a6e523965b2 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/logic/esql_validator.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/logic/esql_validator.ts @@ -36,10 +36,10 @@ const constructValidationError = (error: Error) => { }; /** - * checks whether query has [metadata _id] operator + * checks whether query has metadata _id operator */ export const computeHasMetadataOperator = (esqlQuery: string) => { - return /(? export const ESQL_VALIDATION_MISSING_ID_IN_QUERY_ERROR = i18n.translate( 'xpack.securitySolution.detectionEngine.esqlValidation.missingIdInQueryError', { - defaultMessage: `Queries that don’t use the STATS...BY function (non-aggregating queries) must include the [metadata _id, _version, _index] operator after the source command. For example: FROM logs* [metadata _id, _version, _index]. In addition, the metadata properties (_id, _version, and _index) must be returned in the query response.`, + defaultMessage: `Queries that don’t use the STATS...BY function (non-aggregating queries) must include the "metadata _id, _version, _index" operator after the source command. For example: FROM logs* metadata _id, _version, _index. In addition, the metadata properties (_id, _version, and _index) must be returned in the query response.`, } ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/hooks/use_esql_index.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/hooks/use_esql_index.test.ts index cde92602611ac..dc4394be257e5 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/hooks/use_esql_index.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/hooks/use_esql_index.test.ts @@ -8,7 +8,7 @@ import { renderHook } from '@testing-library/react-hooks'; import { useEsqlIndex } from './use_esql_index'; -const validEsqlQuery = 'from auditbeat* [metadata _id, _index, _version]'; +const validEsqlQuery = 'from auditbeat* metadata _id, _index, _version'; describe('useEsqlIndex', () => { it('should return empty array if isQueryReadEnabled is undefined', () => { const { result } = renderHook(() => useEsqlIndex(validEsqlQuery, 'esql', undefined)); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/hooks/use_investigation_fields.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/hooks/use_investigation_fields.test.ts index 4819f87d5a41f..6a159f87d89d8 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/hooks/use_investigation_fields.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/hooks/use_investigation_fields.test.ts @@ -28,7 +28,7 @@ const fetchFieldsFromESQLMock = fetchFieldsFromESQL as jest.Mock; const { wrapper } = createQueryWrapperMock(); -const mockEsqlQuery = 'from auditbeat* [metadata _id]'; +const mockEsqlQuery = 'from auditbeat* metadata _id'; const mockIndexPatternFields: DataViewFieldBase[] = [ { name: 'agent.name', diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts index 7fd34aff67690..c629f5056ef3a 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts @@ -66,7 +66,7 @@ export default ({ getService }: FtrProviderContext) => { const interval: [string, string] = ['2020-10-28T06:00:00.000Z', '2020-10-28T06:10:00.000Z']; const doc1 = { agent: { name: 'test-1' } }; const doc2 = { agent: { name: 'test-2' } }; - const ruleQuery = `from ecs_compliant [metadata _id, _index, _version] ${internalIdPipe( + const ruleQuery = `from ecs_compliant metadata _id, _index, _version ${internalIdPipe( id )} | where agent.name=="test-1"`; const rule: EsqlRuleCreateProps = { @@ -243,7 +243,7 @@ export default ({ getService }: FtrProviderContext) => { const rule: EsqlRuleCreateProps = { ...getCreateEsqlRulesSchemaMock('rule-1', true), // only _id and agent.name is projected at the end of query pipeline - query: `from ecs_compliant [metadata _id] ${internalIdPipe(id)} | keep _id, agent.name`, + query: `from ecs_compliant metadata _id ${internalIdPipe(id)} | keep _id, agent.name`, from: 'now-1h', interval: '1h', }; @@ -278,6 +278,44 @@ export default ({ getService }: FtrProviderContext) => { ); }); + it('should support deprecated [metadata _id] syntax', async () => { + const id = uuidv4(); + const interval: [string, string] = ['2020-10-28T06:00:00.000Z', '2020-10-28T06:10:00.000Z']; + const doc1 = { + agent: { name: 'test-1', version: '2', type: 'auditbeat' }, + host: { name: 'my-host' }, + client: { ip: '127.0.0.1' }, + }; + + const rule: EsqlRuleCreateProps = { + ...getCreateEsqlRulesSchemaMock('rule-1', true), + // only _id and agent.name is projected at the end of query pipeline + query: `from ecs_compliant [metadata _id] ${internalIdPipe(id)} | keep _id, agent.name`, + from: 'now-1h', + interval: '1h', + }; + + await indexEnhancedDocuments({ + documents: [doc1], + interval, + id, + }); + + const { previewId } = await previewRule({ + supertest, + rule, + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + }); + + const previewAlerts = await getPreviewAlerts({ + es, + previewId, + size: 10, + }); + + expect(previewAlerts.length).toBe(1); + }); + it('should deduplicate alerts correctly based on source document _id', async () => { const id = uuidv4(); // document will fall into 2 rule execution windows @@ -290,7 +328,7 @@ export default ({ getService }: FtrProviderContext) => { const rule: EsqlRuleCreateProps = { ...getCreateEsqlRulesSchemaMock('rule-1', true), // only _id and agent.name is projected at the end of query pipeline - query: `from ecs_compliant [metadata _id] ${internalIdPipe(id)} | keep _id, agent.name`, + query: `from ecs_compliant metadata _id ${internalIdPipe(id)} | keep _id, agent.name`, from: 'now-45m', interval: '30m', }; @@ -725,7 +763,7 @@ export default ({ getService }: FtrProviderContext) => { const id = uuidv4(); const rule: EsqlRuleCreateProps = { ...getCreateEsqlRulesSchemaMock(`rule-${id}`, true), - query: `from ecs_compliant [metadata _id] ${internalIdPipe( + query: `from ecs_compliant metadata _id ${internalIdPipe( id )} | keep _id, agent.name | sort agent.name`, from: '2020-10-28T05:15:00.000Z', @@ -913,7 +951,7 @@ export default ({ getService }: FtrProviderContext) => { const rule: EsqlRuleCreateProps = { ...getCreateEsqlRulesSchemaMock('rule-1', true), - query: `from ecs_compliant [metadata _id] ${internalIdPipe( + query: `from ecs_compliant metadata _id ${internalIdPipe( id )} | where agent.name=="test-1"`, from: 'now-1h', @@ -956,7 +994,7 @@ export default ({ getService }: FtrProviderContext) => { const rule: EsqlRuleCreateProps = { ...getCreateEsqlRulesSchemaMock('rule-1', true), - query: `from ecs_compliant [metadata _id] ${internalIdPipe( + query: `from ecs_compliant metadata _id ${internalIdPipe( id )} | where agent.name=="test-1"`, from: 'now-1h', @@ -1021,7 +1059,7 @@ export default ({ getService }: FtrProviderContext) => { const rule: EsqlRuleCreateProps = { ...getCreateEsqlRulesSchemaMock('rule-1', true), - query: `from ecs_non_compliant [metadata _id] ${internalIdPipe(id)}`, + query: `from ecs_non_compliant metadata _id ${internalIdPipe(id)}`, from: 'now-1h', interval: '1h', }; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule_ess.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule_ess.cy.ts index 945cf43967cbe..2e95bb19a0477 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule_ess.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule_ess.cy.ts @@ -138,7 +138,7 @@ describe('Detection ES|QL rules, creation', { tags: ['@ess'] }, () => { cy.get(ESQL_QUERY_BAR).should('not.be.visible'); }); - it('shows error when non-aggregating ES|QL query does not [metadata] operator', function () { + it('shows error when non-aggregating ES|QL query does not have metadata operator', function () { workaroundForResizeObserver(); const invalidNonAggregatingQuery = 'from auditbeat* | limit 5'; @@ -148,7 +148,7 @@ describe('Detection ES|QL rules, creation', { tags: ['@ess'] }, () => { getDefineContinueButton().click(); cy.get(ESQL_QUERY_BAR).contains( - 'must include the [metadata _id, _version, _index] operator after the source command' + 'must include the "metadata _id, _version, _index" operator after the source command' ); }); @@ -156,7 +156,7 @@ describe('Detection ES|QL rules, creation', { tags: ['@ess'] }, () => { workaroundForResizeObserver(); const invalidNonAggregatingQuery = - 'from auditbeat* [metadata _id, _version, _index] | keep agent.* | limit 5'; + 'from auditbeat* metadata _id, _version, _index | keep agent.* | limit 5'; selectEsqlRuleType(); expandEsqlQueryBar(); @@ -164,14 +164,14 @@ describe('Detection ES|QL rules, creation', { tags: ['@ess'] }, () => { getDefineContinueButton().click(); cy.get(ESQL_QUERY_BAR).contains( - 'must include the [metadata _id, _version, _index] operator after the source command' + 'must include the "metadata _id, _version, _index" operator after the source command' ); }); it('shows error when ES|QL query is invalid', function () { workaroundForResizeObserver(); const invalidEsqlQuery = - 'from auditbeat* [metadata _id, _version, _index] | not_existing_operator'; + 'from auditbeat* metadata _id, _version, _index | not_existing_operator'; visit(CREATE_RULE_URL); selectEsqlRuleType(); @@ -191,7 +191,7 @@ describe('Detection ES|QL rules, creation', { tags: ['@ess'] }, () => { it('shows custom ES|QL field in investigation fields autocomplete and saves it in rule', function () { const CUSTOM_ESQL_FIELD = '_custom_agent_name'; const queryWithCustomFields = [ - `from auditbeat* [metadata _id, _version, _index]`, + `from auditbeat* metadata _id, _version, _index`, `eval ${CUSTOM_ESQL_FIELD} = agent.name`, `keep _id, _custom_agent_name`, `limit 5`,