- -
-
-

- Create your first index lifecycle policy -

-
+
+
-

- An index lifecycle policy helps you manage your indices as they age. -

-
-
- + class="emotion-euiButtonDisplayContent" + > + + Create policy + + +
-
+ `; exports[`policy table sorts when linked index templates header is clicked 1`] = ` diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx index 1290304ef6165..2bad87b149e0f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx @@ -7,12 +7,7 @@ import React, { useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { - EuiButton, - EuiEmptyPrompt, - EuiLoadingSpinner, - EuiPageContent_Deprecated as EuiPageContent, -} from '@elastic/eui'; +import { EuiButton, EuiLoadingSpinner, EuiPageTemplate } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { MIN_SEARCHABLE_SNAPSHOT_LICENSE } from '../../../../common/constants'; @@ -45,47 +40,44 @@ export const EditPolicy: React.FunctionComponent - } - body={ - - } - /> - + } + body={ + + } + /> ); } if (error || !policies) { const { statusCode, message } = error ? error : { statusCode: '', message: '' }; return ( - - - - - } - body={ -

- {message} ({statusCode}) -

- } - actions={ - - - - } - /> -
+ + + + } + body={ +

+ {message} ({statusCode}) +

+ } + actions={ + + + + } + /> ); } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/policy_list.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/policy_list.container.tsx index 7113b00cf4ec2..669f22ccb9b3f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/policy_list.container.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/policy_list.container.tsx @@ -6,12 +6,7 @@ */ import React, { useEffect } from 'react'; -import { - EuiButton, - EuiEmptyPrompt, - EuiLoadingSpinner, - EuiPageContent_Deprecated as EuiPageContent, -} from '@elastic/eui'; +import { EuiButton, EuiLoadingSpinner, EuiPageTemplate } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { PolicyList as PresentationComponent } from './policy_list'; import { useKibana } from '../../../shared_imports'; @@ -30,47 +25,44 @@ export const PolicyList: React.FunctionComponent = () => { if (isLoading) { return ( - - } - body={ - - } - /> - + } + body={ + + } + /> ); } if (error) { const { statusCode, message } = error ? error : { statusCode: '', message: '' }; return ( - - - - - } - body={ -

- {message} ({statusCode}) -

- } - actions={ - - - - } - /> -
+ + + + } + body={ +

+ {message} ({statusCode}) +

+ } + actions={ + + + + } + /> ); } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/policy_list.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/policy_list.tsx index 09fbc2d1c41c6..0a81f6b16bf43 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/policy_list.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/policy_list.tsx @@ -8,13 +8,7 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiButton, - EuiEmptyPrompt, - EuiSpacer, - EuiPageHeader, - EuiPageContent_Deprecated as EuiPageContent, -} from '@elastic/eui'; +import { EuiButton, EuiSpacer, EuiPageHeader, EuiPageTemplate } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public'; import { PolicyFromES } from '../../../../common/types'; @@ -46,30 +40,28 @@ export const PolicyList: React.FunctionComponent = ({ policies, updatePol if (policies.length === 0) { return ( - - + + + + } + body={ + +

- - } - body={ - -

- -

-
- } - actions={createPolicyButton} - /> -
+

+ + } + actions={createPolicyButton} + /> ); } diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx index a7e9a504eb8cc..548577c486aca 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx @@ -8,11 +8,7 @@ import React, { useState, useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiPageContentBody_Deprecated as EuiPageContentBody, - EuiSpacer, - EuiPageHeader, -} from '@elastic/eui'; +import { EuiPageSection, EuiSpacer, EuiPageHeader } from '@elastic/eui'; import { ComponentTemplateDeserialized } from '../../shared_imports'; import { useComponentTemplatesContext } from '../../component_templates_context'; @@ -63,7 +59,7 @@ export const ComponentTemplateCreate: React.FunctionComponent + @@ -85,6 +81,6 @@ export const ComponentTemplateCreate: React.FunctionComponent - + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx index 38e45ade11d01..8fa4694ee033a 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx @@ -9,11 +9,7 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n-react'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; -import { - EuiPageContentBody_Deprecated as EuiPageContentBody, - EuiPageHeader, - EuiSpacer, -} from '@elastic/eui'; +import { EuiPageSection, EuiPageHeader, EuiSpacer } from '@elastic/eui'; import { History } from 'history'; import { useComponentTemplatesContext } from '../../component_templates_context'; @@ -165,7 +161,7 @@ export const ComponentTemplateEdit: React.FunctionComponent + @@ -192,6 +188,6 @@ export const ComponentTemplateEdit: React.FunctionComponent - + ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx index ecce9b92ffc2f..0d17936ec7553 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx @@ -16,7 +16,7 @@ import { EuiText, EuiIconTip, EuiSpacer, - EuiPageContent_Deprecated as EuiPageContent, + EuiPageSection, EuiEmptyPrompt, EuiLink, } from '@elastic/eui'; @@ -270,7 +270,7 @@ export const DataStreamList: React.FunctionComponent + {renderHeader()} @@ -285,7 +285,7 @@ export const DataStreamList: React.FunctionComponent - + ); } diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js index 65efc158fbf42..5f2bfb3de011b 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js @@ -19,7 +19,7 @@ import { EuiCheckbox, EuiFlexGroup, EuiFlexItem, - EuiPageContent_Deprecated as EuiPageContent, + EuiPageSection, EuiScreenReaderOnly, EuiSpacer, EuiSearchBar, @@ -435,56 +435,43 @@ export class IndexTable extends Component { const hasContent = !indicesLoading && !indicesError; if (!hasContent) { - const renderNoContent = () => { - if (indicesLoading) { - return ( - - - - ); - } - - if (indicesError) { - if (indicesError.status === 403) { - return ( - - } - /> - ); - } + if (indicesLoading) { + return ( + + + + ); + } + if (indicesError) { + if (indicesError.status === 403) { return ( } - error={indicesError.body} /> ); } - }; - return ( - - {renderNoContent()} - - ); + return ( + + } + error={indicesError.body} + /> + ); + } } const { selectedIndicesMap } = this.state; @@ -496,7 +483,7 @@ export class IndexTable extends Component { const { extensionsService } = services; return ( - + @@ -665,7 +652,7 @@ export class IndexTable extends Component { {indices.length > 0 ? this.renderPager() : null} - + ); }} diff --git a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx index eff5cbb554904..25b84a8bc6fde 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiPageContentBody_Deprecated as EuiPageContentBody } from '@elastic/eui'; +import { EuiPageSection } from '@elastic/eui'; import { ScopedHistory } from '@kbn/core/public'; import { PageLoading, PageError, Error } from '../../../shared_imports'; @@ -103,7 +103,7 @@ export const TemplateClone: React.FunctionComponent + - + ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx index e5422ca93db26..6961c223e6993 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiPageContentBody_Deprecated as EuiPageContentBody } from '@elastic/eui'; +import { EuiPageSection } from '@elastic/eui'; import { useLocation } from 'react-router-dom'; import { parse } from 'query-string'; import { ScopedHistory } from '@kbn/core/public'; @@ -57,7 +57,7 @@ export const TemplateCreate: React.FunctionComponent = ({ h }, []); return ( - + = ({ h isLegacy={isLegacy} history={history as ScopedHistory} /> - + ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx index b0a6b95351386..c37a7ce44b672 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx @@ -9,11 +9,7 @@ import React, { useEffect, useState, Fragment } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiPageContentBody_Deprecated as EuiPageContentBody, - EuiSpacer, - EuiCallOut, -} from '@elastic/eui'; +import { EuiPageSection, EuiSpacer, EuiCallOut } from '@elastic/eui'; import { ScopedHistory } from '@kbn/core/public'; import { TemplateDeserialized } from '../../../../common'; @@ -131,7 +127,7 @@ export const TemplateEdit: React.FunctionComponent + {isSystemTemplate && ( - + ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx index c208c72558362..e342e538b1433 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx @@ -13,8 +13,8 @@ import { EuiFlexItem, EuiButtonIcon, EuiSpacer, + RIGHT_ALIGNMENT, } from '@elastic/eui'; -import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useMemo } from 'react'; diff --git a/x-pack/plugins/maps/public/classes/styles/color_palettes.ts b/x-pack/plugins/maps/public/classes/styles/color_palettes.ts index be07f78ff69ae..a4fd893ea8c90 100644 --- a/x-pack/plugins/maps/public/classes/styles/color_palettes.ts +++ b/x-pack/plugins/maps/public/classes/styles/color_palettes.ts @@ -7,26 +7,17 @@ import tinycolor from 'tinycolor2'; import { - // @ts-ignore colorPalette as colorPaletteGenerator, - // @ts-ignore euiPaletteForStatus, - // @ts-ignore euiPaletteForTemperature, - // @ts-ignore euiPaletteCool, - // @ts-ignore euiPaletteWarm, - // @ts-ignore euiPaletteNegative, - // @ts-ignore euiPalettePositive, - // @ts-ignore euiPaletteGray, - // @ts-ignore euiPaletteColorBlind, -} from '@elastic/eui/lib/services'; -import { EuiColorPalettePickerPaletteProps } from '@elastic/eui'; + EuiColorPalettePickerPaletteProps, +} from '@elastic/eui'; import { PercentilesFieldMeta } from '../../../common/descriptor_types'; export const DEFAULT_HEATMAP_COLOR_RAMP_NAME = 'theclassic'; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/color/color_stops_utils.js b/x-pack/plugins/maps/public/classes/styles/vector/components/color/color_stops_utils.js index 3627181215471..a92b6db671fcf 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/color/color_stops_utils.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/color/color_stops_utils.js @@ -5,9 +5,8 @@ * 2.0. */ -import { isValidHex } from '@elastic/eui'; +import { isValidHex, euiPaletteColorBlind } from '@elastic/eui'; import _ from 'lodash'; -import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; const DEFAULT_CUSTOM_PALETTE = euiPaletteColorBlind({ rotations: 3 }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/extract_color_from_style_property.test.ts b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/extract_color_from_style_property.test.ts index 92e897842ac31..aeeeff361d5a0 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/extract_color_from_style_property.test.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/extract_color_from_style_property.test.ts @@ -13,8 +13,7 @@ import { ColorStaticStylePropertyDescriptor, } from '../../../../../../common/descriptor_types'; import { COLOR_MAP_TYPE, FIELD_ORIGIN, STYLE_TYPE } from '../../../../../../common/constants'; -// @ts-ignore -import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; +import { euiPaletteColorBlind } from '@elastic/eui'; const blue = '#0000ff'; const yellow = '#ffff00'; diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index adce983312d15..f9aa282b4b118 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -25,12 +25,11 @@ import { EuiLink, EuiLoadingSpinner, EuiToolTip, + RIGHT_ALIGNMENT, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; - import { addItemToRecentlyAccessed } from '../../../util/recently_accessed'; import { ml } from '../../../services/ml_api_service'; import { mlJobService } from '../../../services/job_service'; diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js index 49cd964c09db3..40ebecd135548 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js @@ -19,9 +19,11 @@ import { EuiCallOut, EuiButton, EuiText, + LEFT_ALIGNMENT, + CENTER_ALIGNMENT, + SortableProperties, } from '@elastic/eui'; -import { LEFT_ALIGNMENT, CENTER_ALIGNMENT, SortableProperties } from '@elastic/eui/lib/services'; import { i18n } from '@kbn/i18n'; import { useMlKibana } from '../../../contexts/kibana'; import { ML_PAGES } from '../../../../../common/constants/locator'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx index b5bf6b012750a..1b2f83ef58fce 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx @@ -6,10 +6,16 @@ */ import React, { FC, Fragment, useEffect, useState } from 'react'; -import { EuiCallOut, EuiFormRow, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; +import { + EuiCallOut, + EuiFormRow, + EuiPanel, + EuiSpacer, + EuiText, + LEFT_ALIGNMENT, + SortableProperties, +} from '@elastic/eui'; import { isEqual } from 'lodash'; -// @ts-ignore no declaration -import { LEFT_ALIGNMENT, SortableProperties } from '@elastic/eui/lib/services'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { ES_FIELD_TYPES } from '@kbn/field-types'; @@ -54,7 +60,7 @@ export const AnalysisFieldsTable: FC<{ unsupportedFieldsError, setUnsupportedFieldsError, }) => { - const [sortableProperties, setSortableProperties] = useState(); + const [sortableProperties, setSortableProperties] = useState>(); const [currentPaginationData, setCurrentPaginationData] = useState<{ pageIndex: number; itemsPerPage: number; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js index 2488d55b0a548..9bdc8c4c40abb 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js @@ -16,8 +16,8 @@ import { EuiInMemoryTable, EuiLink, EuiLoadingSpinner, + formatNumber, } from '@elastic/eui'; -import { formatNumber } from '@elastic/eui/lib/services/format'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/manifest.json index 11d4f8e0b97df..0108292be19b7 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/manifest.json @@ -7,7 +7,8 @@ "defaultIndexPattern": "kibana_sample_data_ecommerce", "query": { "bool": { - "filter": [{ "term": { "event.dataset": "sample_ecommerce" } }] + "filter": [{ "term": { "event.dataset": "sample_ecommerce" } }], + "must_not": { "terms": { "_tier": [ "data_frozen", "data_cold" ] } } } }, "jobs": [ diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/manifest.json index 446f56a717e11..bb85d26b72f41 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/manifest.json @@ -7,7 +7,8 @@ "defaultIndexPattern": "kibana_sample_data_logs", "query": { "bool": { - "filter": [{ "term": { "event.dataset": "sample_web_logs" } }] + "filter": [{ "term": { "event.dataset": "sample_web_logs" } }], + "must_not": { "terms": { "_tier": [ "data_frozen", "data_cold" ] } } } }, "jobs": [ diff --git a/x-pack/plugins/monitoring/public/components/chart/get_color.js b/x-pack/plugins/monitoring/public/components/chart/get_color.js index 49a98f38e9c2b..7e8cac5540985 100644 --- a/x-pack/plugins/monitoring/public/components/chart/get_color.js +++ b/x-pack/plugins/monitoring/public/components/chart/get_color.js @@ -14,7 +14,7 @@ * @param {Integer} index: index of the chart series, 0-3 * @returns {String} Hex color to use for chart series at the given index */ -import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; +import { euiPaletteColorBlind } from '@elastic/eui'; export function getColor(app, index) { let seriesColors; diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.test.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.test.ts index 38e95e9d75323..8737c80e969f8 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.test.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.test.ts @@ -27,7 +27,7 @@ describe('ReportingAPIClient', () => { describe('getReportURL', () => { it('should generate the internal report download URL', () => { expect(apiClient.getReportURL('123')).toMatchInlineSnapshot( - `"/base/path/internal/reporting/jobs/download/123"` + `"/base/path/internal/reporting/jobs/download/123?elasticInternalOrigin=true"` ); }); }); diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts index 2681789745751..7e30866474991 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { ELASTIC_INTERNAL_ORIGIN_QUERY_PARAM } from '@kbn/core-http-common'; import type { HttpFetchQuery } from '@kbn/core/public'; import { HttpSetup, IUiSettingsClient } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; @@ -17,13 +18,7 @@ import { PUBLIC_ROUTES, REPORTING_MANAGEMENT_HOME, } from '../../../common/constants'; -import { - BaseParams, - DownloadReportFn, - JobId, - ManagementLinkFn, - ReportApiJSON, -} from '../../../common/types'; +import { BaseParams, JobId, ManagementLinkFn, ReportApiJSON } from '../../../common/types'; import { add } from '../../notifier/job_completion_notifications'; import { Job } from '../job'; @@ -58,7 +53,6 @@ interface IReportingAPI { // Function props getManagementLink: ManagementLinkFn; - getDownloadLink: DownloadReportFn; // Diagnostic-related API calls verifyBrowser(): Promise; @@ -98,7 +92,7 @@ export class ReportingAPIClient implements IReportingAPI { */ public getReportURL(jobId: string) { const downloadLink = this.http.basePath.prepend( - `${INTERNAL_ROUTES.JOBS.DOWNLOAD_PREFIX}/${jobId}` + `${INTERNAL_ROUTES.JOBS.DOWNLOAD_PREFIX}/${jobId}?${ELASTIC_INTERNAL_ORIGIN_QUERY_PARAM}=true` ); return downloadLink; @@ -218,8 +212,7 @@ export class ReportingAPIClient implements IReportingAPI { public getManagementLink: ManagementLinkFn = () => this.http.basePath.prepend(REPORTING_MANAGEMENT_HOME); - public getDownloadLink: DownloadReportFn = (jobId: JobId) => - this.http.basePath.prepend(`${INTERNAL_ROUTES.JOBS.DOWNLOAD_PREFIX}/${jobId}`); + public getDownloadLink = (jobId: JobId) => this.getReportURL(jobId); public getServerBasePath = () => this.http.basePath.serverBasePath; diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts index 6f575652450c1..ed3b973a95e64 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts @@ -34,7 +34,7 @@ jobQueueClientMock.getInfo = () => Promise.resolve({ content: 'this is the completed report data' } as unknown as Job); jobQueueClientMock.getError = () => Promise.resolve('this is the failed report error'); jobQueueClientMock.getManagementLink = () => '/#management'; -jobQueueClientMock.getDownloadLink = () => '/reporting/download/job-123'; +jobQueueClientMock.getReportURL = () => '/reporting/download/job-123'; const mockShowDanger = stub(); const mockShowSuccess = stub(); diff --git a/x-pack/plugins/reporting/tsconfig.json b/x-pack/plugins/reporting/tsconfig.json index 05e62e6251e19..f307442001655 100644 --- a/x-pack/plugins/reporting/tsconfig.json +++ b/x-pack/plugins/reporting/tsconfig.json @@ -40,6 +40,7 @@ "@kbn/reporting-common", "@kbn/saved-search-plugin", "@kbn/core-http-router-server-internal", + "@kbn/core-http-common", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/screenshotting/server/__mocks__/puppeteer.ts b/x-pack/plugins/screenshotting/server/__mocks__/puppeteer.ts new file mode 100644 index 0000000000000..f295192723c2b --- /dev/null +++ b/x-pack/plugins/screenshotting/server/__mocks__/puppeteer.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +const stubDevTools = { + send: jest.fn(), +}; +const stubTarget = { + createCDPSession: jest.fn(() => { + return stubDevTools; + }), +}; +const stubPage = { + target: jest.fn(() => { + return stubTarget; + }), + emulateTimezone: jest.fn(), + setDefaultTimeout: jest.fn(), + isClosed: jest.fn(), + setViewport: jest.fn(), + evaluate: jest.fn(), + screenshot: jest.fn().mockResolvedValue(`you won't believe this one weird screenshot`), + evaluateOnNewDocument: jest.fn(), + setRequestInterception: jest.fn(), + _client: jest.fn(() => ({ on: jest.fn() })), + on: jest.fn(), + goto: jest.fn(), + waitForSelector: jest.fn().mockResolvedValue(true), + waitForFunction: jest.fn(), +}; +const stubBrowser = { + newPage: jest.fn(() => { + return stubPage; + }), +}; + +const puppeteer = { + launch: jest.fn(() => { + return stubBrowser; + }), +}; + +// eslint-disable-next-line import/no-default-export +export default puppeteer; diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/driver.test.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver.test.ts new file mode 100644 index 0000000000000..fb07eac64ed17 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver.test.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from '@kbn/logging'; +import { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/server'; +import * as puppeteer from 'puppeteer'; +import { Size } from '../../../common/layout'; +import { ConfigType } from '../../config'; +import { PreserveLayout } from '../../layouts/preserve_layout'; +import { HeadlessChromiumDriver } from './driver'; + +describe('chromium driver', () => { + let mockConfig: ConfigType; + let mockLogger: Logger; + let mockScreenshotModeSetup: ScreenshotModePluginSetup; + let mockPage: puppeteer.Page; + + const mockBasePath = '/kibanaTest1'; + + beforeEach(() => { + mockLogger = { debug: jest.fn(), error: jest.fn(), info: jest.fn() } as unknown as Logger; + mockLogger.get = () => mockLogger; + + mockConfig = { + networkPolicy: { + enabled: false, + rules: [], + }, + browser: { + autoDownload: false, + chromium: { proxy: { enabled: false } }, + }, + capture: { + timeouts: { + openUrl: 60000, + waitForElements: 60000, + renderComplete: 60000, + }, + zoom: 2, + }, + poolSize: 1, + }; + + mockPage = { + screenshot: jest.fn().mockResolvedValue(`you won't believe this one weird screenshot`), + evaluate: jest.fn(), + } as unknown as puppeteer.Page; + + mockScreenshotModeSetup = { + setScreenshotContext: jest.fn(), + setScreenshotModeEnabled: jest.fn(), + isScreenshotMode: jest.fn(), + }; + }); + + it('return screenshot with preserve layout option', async () => { + const driver = new HeadlessChromiumDriver( + mockScreenshotModeSetup, + mockConfig, + mockBasePath, + mockPage + ); + + const result = await driver.screenshot({ + elementPosition: { + boundingClientRect: { top: 200, left: 10, height: 10, width: 100 }, + scroll: { x: 100, y: 300 }, + }, + layout: new PreserveLayout({ width: 16, height: 16 }), + }); + + expect(result).toEqual(Buffer.from(`you won't believe this one weird screenshot`, 'base64')); + }); + + it('add error to screenshot contents', async () => { + const driver = new HeadlessChromiumDriver( + mockScreenshotModeSetup, + mockConfig, + mockBasePath, + mockPage + ); + + // @ts-expect-error spy on non-public class method + const testSpy = jest.spyOn(driver, 'injectScreenshottingErrorHeader'); + + const result = await driver.screenshot({ + elementPosition: { + boundingClientRect: { top: 200, left: 10, height: 10, width: 100 }, + scroll: { x: 100, y: 300 }, + }, + layout: new PreserveLayout({} as Size), + error: new Error(`Here's the fake error!`), + }); + + expect(testSpy.mock.lastCall).toMatchInlineSnapshot(` + Array [ + [Error: Here's the fake error!], + "[data-shared-items-container]", + ] + `); + expect(result).toEqual(Buffer.from(`you won't believe this one weird screenshot`, 'base64')); + }); +}); diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.test.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.test.ts index 325a734edd741..c3e6f75d6d511 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.test.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.test.ts @@ -6,6 +6,7 @@ */ import type { Logger } from '@kbn/core/server'; +import { loggerMock } from '@kbn/logging-mocks'; import type { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/server'; import * as puppeteer from 'puppeteer'; import * as Rx from 'rxjs'; @@ -24,22 +25,18 @@ describe('HeadlessChromiumDriverFactory', () => { }, }, } as ConfigType; - let logger: jest.Mocked; - let screenshotMode: jest.Mocked; + let logger: Logger; + let screenshotMode: ScreenshotModePluginSetup; let factory: HeadlessChromiumDriverFactory; - let mockBrowser: jest.Mocked; + let mockBrowser: puppeteer.Browser; beforeEach(async () => { - logger = { - debug: jest.fn(), - error: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - get: jest.fn(() => logger), - } as unknown as typeof logger; - screenshotMode = {} as unknown as typeof screenshotMode; + logger = loggerMock.create(); + + screenshotMode = {} as unknown as ScreenshotModePluginSetup; let pageClosed = false; + mockBrowser = { newPage: jest.fn().mockResolvedValue({ target: jest.fn(() => ({ @@ -57,9 +54,8 @@ describe('HeadlessChromiumDriverFactory', () => { pageClosed = true; }), process: jest.fn(), - } as unknown as jest.Mocked; - - (puppeteer as jest.Mocked).launch.mockResolvedValue(mockBrowser); + } as unknown as puppeteer.Browser; + jest.spyOn(puppeteer, 'launch').mockResolvedValue(mockBrowser); factory = new HeadlessChromiumDriverFactory(screenshotMode, config, logger, path, ''); jest.spyOn(factory, 'getBrowserLogger').mockReturnValue(Rx.EMPTY); @@ -84,9 +80,8 @@ describe('HeadlessChromiumDriverFactory', () => { }); it('rejects if Puppeteer launch fails', async () => { - (puppeteer as jest.Mocked).launch.mockRejectedValue( - `Puppeteer Launch mock fail.` - ); + jest.spyOn(puppeteer, 'launch').mockRejectedValue(`Puppeteer Launch mock fail.`); + expect(() => factory .createPage({ openUrlTimeout: 0, defaultViewport: DEFAULT_VIEWPORT }) @@ -99,9 +94,8 @@ describe('HeadlessChromiumDriverFactory', () => { describe('close behaviour', () => { it('does not allow close to be called on the browse more than once', async () => { - await factory - .createPage({ openUrlTimeout: 0, defaultViewport: DEFAULT_VIEWPORT }) - .pipe( + await Rx.firstValueFrom( + factory.createPage({ openUrlTimeout: 0, defaultViewport: DEFAULT_VIEWPORT }).pipe( take(1), mergeMap(async ({ close }) => { expect(mockBrowser.close).not.toHaveBeenCalled(); @@ -110,7 +104,7 @@ describe('HeadlessChromiumDriverFactory', () => { expect(mockBrowser.close).toHaveBeenCalledTimes(1); }) ) - .toPromise(); + ); // Check again, after the observable completes expect(mockBrowser.close).toHaveBeenCalledTimes(1); }); diff --git a/x-pack/plugins/screenshotting/server/screenshots/index.ts b/x-pack/plugins/screenshotting/server/screenshots/index.ts index f8dd839dfc55c..b1a0d98fe8a27 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/index.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/index.ts @@ -5,48 +5,18 @@ * 2.0. */ -import type { CloudSetup } from '@kbn/cloud-plugin/server'; -import type { HttpServiceSetup, KibanaRequest, Logger, PackageInfo } from '@kbn/core/server'; +import type { KibanaRequest } from '@kbn/core/server'; import type { ExpressionAstExpression } from '@kbn/expressions-plugin/common'; import type { Optional } from '@kbn/utility-types'; -import { Semaphore } from '@kbn/std'; -import ipaddr from 'ipaddr.js'; -import { defaultsDeep, sum } from 'lodash'; -import { from, Observable, of, throwError } from 'rxjs'; -import { - catchError, - concatMap, - first, - map, - mergeMap, - take, - takeUntil, - tap, - toArray, -} from 'rxjs/operators'; -import { - errors, - LayoutParams, - SCREENSHOTTING_APP_ID, - SCREENSHOTTING_EXPRESSION, - SCREENSHOTTING_EXPRESSION_INPUT, -} from '../../common'; -import { HeadlessChromiumDriverFactory, PerformanceMetrics } from '../browsers'; -import { systemHasInsufficientMemory } from '../cloud'; -import type { ConfigType } from '../config'; -import { durationToNumber } from '../config'; +import { LayoutParams } from '../../common'; +import { PerformanceMetrics } from '../browsers'; import { PdfScreenshotOptions, PdfScreenshotResult, PngScreenshotOptions, PngScreenshotResult, - toPdf, - toPng, } from '../formats'; -import { createLayout, Layout } from '../layouts'; -import { EventLogger, Transactions } from './event_logger'; import type { ScreenshotObservableOptions, ScreenshotObservableResult } from './observable'; -import { ScreenshotObservableHandler, UrlOrUrlWithContext } from './observable'; export type { ScreenshotObservableResult, UrlOrUrlWithContext } from './observable'; @@ -55,17 +25,14 @@ export interface CaptureOptions extends Optional { - const { browserTimezone } = options; - - return this.browserDriverFactory - .createPage( - { - browserTimezone, - openUrlTimeout: durationToNumber(this.config.capture.timeouts.openUrl), - defaultViewport: { width: layout.width, deviceScaleFactor: layout.getBrowserZoom() }, - }, - this.logger - ) - .pipe( - this.semaphore.acquire(), - mergeMap(({ driver, error$, close }) => { - const screen: ScreenshotObservableHandler = new ScreenshotObservableHandler( - driver, - this.config, - eventLogger, - layout, - options - ); - - return from(options.urls).pipe( - concatMap((url, index) => - screen.setupPage(index, url).pipe( - catchError((error) => { - screen.checkPageIsOpen(); // this fails the job if the browser has closed - - this.logger.error(error); - eventLogger.error(error, Transactions.SCREENSHOTTING); - return of({ ...DEFAULT_SETUP_RESULT, error }); // allow "as-is" screenshot with injected warning message - }), - takeUntil(error$), - screen.getScreenshots() - ) - ), - take(options.urls.length), - toArray(), - mergeMap((results) => - // At this point we no longer need the page, close it and send out the results - close().pipe(map(({ metrics }) => ({ metrics, results }))) - ) - ); - }), - first() - ); - } - - private getScreenshottingAppUrl() { - const info = this.http.getServerInfo(); - const { protocol, port } = info; - let { hostname } = info; - - if (ipaddr.isValid(hostname) && !sum(ipaddr.parse(hostname).toByteArray())) { - hostname = 'localhost'; - } - - return `${protocol}://${hostname}:${port}${this.http.basePath.serverBasePath}/app/${SCREENSHOTTING_APP_ID}`; - } - - private getCaptureOptions({ - expression, - input, - request, - ...options - }: ScreenshotOptions): ScreenshotObservableOptions { - const headers = { ...(request?.headers ?? {}), ...(options.headers ?? {}) }; - const urls = expression - ? [ - [ - this.getScreenshottingAppUrl(), - { - [SCREENSHOTTING_EXPRESSION]: expression, - [SCREENSHOTTING_EXPRESSION_INPUT]: input, - }, - ] as UrlOrUrlWithContext, - ] - : options.urls; - - return defaultsDeep( - { - ...options, - headers, - urls, - }, - { - timeouts: { - openUrl: 60000, - waitForElements: 60000, - renderComplete: 120000, - }, - urls: [], - } - ); - } - - systemHasInsufficientMemory(): boolean { - return systemHasInsufficientMemory(this.cloud, this.logger.get('cloud')); - } - - getScreenshots(options: PngScreenshotOptions): Observable; - getScreenshots(options: PdfScreenshotOptions): Observable; - getScreenshots(options: ScreenshotOptions): Observable; - getScreenshots(options: ScreenshotOptions): Observable { - if (this.systemHasInsufficientMemory()) { - return throwError(() => new errors.InsufficientMemoryAvailableOnCloudError()); - } - - const eventLogger = new EventLogger(this.logger, this.config); - const transactionEnd = eventLogger.startTransaction(Transactions.SCREENSHOTTING); - - const layout = createLayout(options.layout ?? {}); - const captureOptions = this.getCaptureOptions(options); - - return this.captureScreenshots(eventLogger, layout, captureOptions).pipe( - tap(({ results, metrics }) => { - transactionEnd({ - labels: { - cpu: metrics?.cpu, - memory: metrics?.memory, - memory_mb: metrics?.memoryInMegabytes, - ...eventLogger.getByteLengthFromCaptureResults(results), - }, - }); - }), - mergeMap((result) => { - switch (options.format) { - case 'pdf': - return toPdf(eventLogger, this.packageInfo, layout, options, result); - default: - return toPng(result); - } - }) - ); - } -} +export { Screenshots } from './screenshots'; diff --git a/x-pack/plugins/screenshotting/server/screenshots/screenshots.test.ts b/x-pack/plugins/screenshotting/server/screenshots/screenshots.test.ts new file mode 100644 index 0000000000000..d5e8757803c81 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/screenshots.test.ts @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CloudSetup } from '@kbn/cloud-plugin/server'; +import type { HttpServiceSetup } from '@kbn/core-http-server'; +import type { PackageInfo } from '@kbn/core/server'; +import type { Logger } from '@kbn/logging'; +import { loggerMock } from '@kbn/logging-mocks'; +import type { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/server'; +import puppeteer from 'puppeteer'; +import * as Rx from 'rxjs'; +import { firstValueFrom } from 'rxjs'; +import type { PngScreenshotOptions } from '..'; +import { HeadlessChromiumDriverFactory } from '../browsers'; +import type { ConfigType } from '../config'; +import { Screenshots } from './screenshots'; + +jest.mock('puppeteer'); + +describe('class Screenshots', () => { + let mockConfig: ConfigType; + let browserDriverFactory: HeadlessChromiumDriverFactory; + let mockPackageInfo: PackageInfo; + let mockHttpSetup: HttpServiceSetup; + let mockCloudSetup: CloudSetup; + let mockLogger: Logger; + let mockScreenshotModeSetup: ScreenshotModePluginSetup; + + const mockBinaryPath = '/kibana/x-pack/plugins/screenshotting/chromium/linux/headless_shell'; + const mockBasePath = '/kibanaTest1'; + + beforeEach(() => { + mockLogger = loggerMock.create(); + + mockConfig = { + networkPolicy: { + enabled: false, + rules: [], + }, + browser: { + autoDownload: false, + chromium: { proxy: { enabled: false } }, + }, + capture: { + timeouts: { + openUrl: 60000, + waitForElements: 60000, + renderComplete: 60000, + }, + zoom: 2, + }, + poolSize: 1, + }; + + mockScreenshotModeSetup = {} as unknown as ScreenshotModePluginSetup; + + browserDriverFactory = new HeadlessChromiumDriverFactory( + mockScreenshotModeSetup, + mockConfig, + mockLogger, + mockBinaryPath, + mockBasePath + ); + + mockCloudSetup = { isCloudEnabled: true, instanceSizeMb: 8000 } as unknown as CloudSetup; + }); + + const getScreenshotsInstance = () => + new Screenshots( + browserDriverFactory, + mockLogger, + mockPackageInfo, + mockHttpSetup, + mockConfig, + mockCloudSetup + ); + + it('detects sufficient memory from cloud plugin', () => { + const screenshotsInstance = getScreenshotsInstance(); + const hasInsufficient = screenshotsInstance.systemHasInsufficientMemory(); + expect(hasInsufficient).toBe(false); + }); + + it('detects insufficient memory from cloud plugin', () => { + mockCloudSetup = { isCloudEnabled: true, instanceSizeMb: 1000 } as unknown as CloudSetup; + const screenshotsInstance = getScreenshotsInstance(); + const hasInsufficient = screenshotsInstance.systemHasInsufficientMemory(); + expect(hasInsufficient).toBe(true); + }); + + it('ignores insufficient memory if cloud is not enabled', () => { + mockCloudSetup = { isCloudEnabled: false, instanceSizeMb: 1000 } as unknown as CloudSetup; + const screenshotsInstance = getScreenshotsInstance(); + const hasInsufficient = screenshotsInstance.systemHasInsufficientMemory(); + expect(hasInsufficient).toBe(false); + }); + + describe('getScreenshots', () => { + beforeAll(() => { + jest.mock('puppeteer'); // see __mocks__/puppeteer.ts + }); + + beforeEach(() => { + jest.spyOn(browserDriverFactory, 'getBrowserLogger').mockReturnValue(Rx.EMPTY); + jest.spyOn(browserDriverFactory, 'getProcessLogger').mockReturnValue(Rx.EMPTY); + jest.spyOn(browserDriverFactory, 'getPageExit').mockReturnValue(Rx.EMPTY); + }); + + it('getScreenshots with PngScreenshotOptions', async () => { + const screenshotsInstance = getScreenshotsInstance(); + + const options: PngScreenshotOptions = { + format: 'png', + layout: { id: 'preserve_layout' }, + urls: ['/app/home/test'], + }; + + const observe = screenshotsInstance.getScreenshots(options); + await firstValueFrom(observe).then((captureResult) => { + expect(captureResult.results[0].screenshots[0].data).toEqual( + Buffer.from(`you won't believe this one weird screenshot`, 'base64') + ); + expect(captureResult.results[0].renderErrors).toBe(undefined); + expect(captureResult.results[0].error).toBe(undefined); + }); + }); + + it('adds warning to the screenshot in case of openUrl timeout', async () => { + // @ts-expect-error should not assign new value to read-only property + mockConfig.capture.timeouts.openUrl = 10; // must be a small amount of milliseconds + + // mock override + const browser = await puppeteer.launch(); + const page = await browser.newPage(); // should be stubPage + const pageGotoSpy = jest.spyOn(page, 'goto'); + pageGotoSpy.mockImplementation( + () => + new Promise((resolve) => { + setTimeout(resolve, 100); // must be larger than 10 + }) + ); + + const screenshotsInstance = getScreenshotsInstance(); + + const options: PngScreenshotOptions = { + format: 'png', + layout: { id: 'preserve_layout' }, + urls: ['/app/home/test'], + }; + + const observe = screenshotsInstance.getScreenshots(options); + await firstValueFrom(observe).then((captureResult) => { + expect(captureResult.results[0].error).toEqual( + new Error( + `Screenshotting encountered a timeout error: "open URL" took longer than 0.01 seconds.` + + ` You may need to increase "xpack.screenshotting.capture.timeouts.openUrl" in kibana.yml.` + ) + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/screenshotting/server/screenshots/screenshots.ts b/x-pack/plugins/screenshotting/server/screenshots/screenshots.ts new file mode 100644 index 0000000000000..64f6ecdb5264a --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/screenshots.ts @@ -0,0 +1,208 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CloudSetup } from '@kbn/cloud-plugin/server'; +import type { HttpServiceSetup, Logger, PackageInfo } from '@kbn/core/server'; +import { Semaphore } from '@kbn/std'; +import ipaddr from 'ipaddr.js'; +import { defaultsDeep, sum } from 'lodash'; +import { from, Observable, of, throwError } from 'rxjs'; +import { + catchError, + concatMap, + first, + map, + mergeMap, + take, + takeUntil, + tap, + toArray, +} from 'rxjs/operators'; +import { CaptureResult, ScreenshotOptions, ScreenshotResult } from '.'; +import { + errors, + SCREENSHOTTING_APP_ID, + SCREENSHOTTING_EXPRESSION, + SCREENSHOTTING_EXPRESSION_INPUT, +} from '../../common'; +import { HeadlessChromiumDriverFactory } from '../browsers'; +import { systemHasInsufficientMemory } from '../cloud'; +import type { ConfigType } from '../config'; +import { durationToNumber } from '../config'; +import { + PdfScreenshotOptions, + PdfScreenshotResult, + PngScreenshotOptions, + PngScreenshotResult, + toPdf, + toPng, +} from '../formats'; +import { createLayout, Layout } from '../layouts'; +import { EventLogger, Transactions } from './event_logger'; +import type { ScreenshotObservableOptions } from './observable'; +import { ScreenshotObservableHandler, UrlOrUrlWithContext } from './observable'; + +const DEFAULT_SETUP_RESULT = { + elementsPositionAndAttributes: null, + timeRange: null, +}; + +export class Screenshots { + private semaphore: Semaphore; + + constructor( + private readonly browserDriverFactory: HeadlessChromiumDriverFactory, + private readonly logger: Logger, + private readonly packageInfo: PackageInfo, + private readonly http: HttpServiceSetup, + private readonly config: ConfigType, + private readonly cloud?: CloudSetup + ) { + this.semaphore = new Semaphore(config.poolSize); + } + + private captureScreenshots( + eventLogger: EventLogger, + layout: Layout, + options: ScreenshotObservableOptions + ): Observable { + const { browserTimezone } = options; + + return this.browserDriverFactory + .createPage( + { + browserTimezone, + openUrlTimeout: durationToNumber(this.config.capture.timeouts.openUrl), + defaultViewport: { width: layout.width, deviceScaleFactor: layout.getBrowserZoom() }, + }, + this.logger + ) + .pipe( + this.semaphore.acquire(), + mergeMap(({ driver, error$, close }) => { + const screen: ScreenshotObservableHandler = new ScreenshotObservableHandler( + driver, + this.config, + eventLogger, + layout, + options + ); + + return from(options.urls).pipe( + concatMap((url, index) => + screen.setupPage(index, url).pipe( + catchError((error) => { + screen.checkPageIsOpen(); // this fails the job if the browser has closed + + this.logger.error(error); + eventLogger.error(error, Transactions.SCREENSHOTTING); + return of({ ...DEFAULT_SETUP_RESULT, error }); // allow "as-is" screenshot with injected warning message + }), + takeUntil(error$), + screen.getScreenshots() + ) + ), + take(options.urls.length), + toArray(), + mergeMap((results) => + // At this point we no longer need the page, close it and send out the results + close().pipe(map(({ metrics }) => ({ metrics, results }))) + ) + ); + }), + first() + ); + } + + private getScreenshottingAppUrl() { + const info = this.http.getServerInfo(); + const { protocol, port } = info; + let { hostname } = info; + + if (ipaddr.isValid(hostname) && !sum(ipaddr.parse(hostname).toByteArray())) { + hostname = 'localhost'; + } + + return `${protocol}://${hostname}:${port}${this.http.basePath.serverBasePath}/app/${SCREENSHOTTING_APP_ID}`; + } + + private getCaptureOptions({ + expression, + input, + request, + ...options + }: ScreenshotOptions): ScreenshotObservableOptions { + const headers = { ...(request?.headers ?? {}), ...(options.headers ?? {}) }; + const urls = expression + ? [ + [ + this.getScreenshottingAppUrl(), + { + [SCREENSHOTTING_EXPRESSION]: expression, + [SCREENSHOTTING_EXPRESSION_INPUT]: input, + }, + ] as UrlOrUrlWithContext, + ] + : options.urls; + + return defaultsDeep( + { + ...options, + headers, + urls, + }, + { + timeouts: { + openUrl: 60000, + waitForElements: 60000, + renderComplete: 120000, + }, + urls: [], + } + ); + } + + systemHasInsufficientMemory(): boolean { + return systemHasInsufficientMemory(this.cloud, this.logger.get('cloud')); + } + + getScreenshots(options: PngScreenshotOptions): Observable; + getScreenshots(options: PdfScreenshotOptions): Observable; + getScreenshots(options: ScreenshotOptions): Observable; + getScreenshots(options: ScreenshotOptions): Observable { + if (this.systemHasInsufficientMemory()) { + return throwError(() => new errors.InsufficientMemoryAvailableOnCloudError()); + } + + const eventLogger = new EventLogger(this.logger, this.config); + const transactionEnd = eventLogger.startTransaction(Transactions.SCREENSHOTTING); + + const layout = createLayout(options.layout ?? {}); + const captureOptions = this.getCaptureOptions(options); + + return this.captureScreenshots(eventLogger, layout, captureOptions).pipe( + tap(({ results, metrics }) => { + transactionEnd({ + labels: { + cpu: metrics?.cpu, + memory: metrics?.memory, + memory_mb: metrics?.memoryInMegabytes, + ...eventLogger.getByteLengthFromCaptureResults(results), + }, + }); + }), + mergeMap((result) => { + switch (options.format) { + case 'pdf': + return toPdf(eventLogger, this.packageInfo, layout, options, result); + default: + return toPng(result); + } + }) + ); + } +} diff --git a/x-pack/plugins/screenshotting/tsconfig.json b/x-pack/plugins/screenshotting/tsconfig.json index 9110ea16661c2..3749a60fc4fe4 100644 --- a/x-pack/plugins/screenshotting/tsconfig.json +++ b/x-pack/plugins/screenshotting/tsconfig.json @@ -23,6 +23,8 @@ "@kbn/utils", "@kbn/safer-lodash-set", "@kbn/core-logging-server-mocks", + "@kbn/logging-mocks", + "@kbn/core-http-server", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/security/server/authentication/unauthenticated_page.tsx b/x-pack/plugins/security/server/authentication/unauthenticated_page.tsx index df26a7b802d4d..fce29bbe89bc3 100644 --- a/x-pack/plugins/security/server/authentication/unauthenticated_page.tsx +++ b/x-pack/plugins/security/server/authentication/unauthenticated_page.tsx @@ -5,8 +5,7 @@ * 2.0. */ -// @ts-expect-error no definitions in component folder -import { EuiButton } from '@elastic/eui/lib/components/button'; +import { EuiButton } from '@elastic/eui'; import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; diff --git a/x-pack/plugins/security/server/authorization/reset_session_page.tsx b/x-pack/plugins/security/server/authorization/reset_session_page.tsx index 30fafabe0ae43..9cb85f324fab9 100644 --- a/x-pack/plugins/security/server/authorization/reset_session_page.tsx +++ b/x-pack/plugins/security/server/authorization/reset_session_page.tsx @@ -5,8 +5,7 @@ * 2.0. */ -// @ts-expect-error no definitions in component folder -import { EuiButton, EuiButtonEmpty } from '@elastic/eui/lib/components/button'; +import { EuiButton, EuiButtonEmpty } from '@elastic/eui'; import React from 'react'; import type { IBasePath } from '@kbn/core/server'; diff --git a/x-pack/plugins/security_solution/public/app/home/index.test.tsx b/x-pack/plugins/security_solution/public/app/home/index.test.tsx index 83ac458032c2c..d9e940f0c2f07 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.test.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.test.tsx @@ -625,7 +625,7 @@ describe('HomePage', () => { ); }); - it('it removes empty timeline state from URL', async () => { + it('it keeps timeline visibility and selected tab state in URL', async () => { const { storage } = createSecuritySolutionStorageMock(); const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); @@ -649,7 +649,11 @@ describe('HomePage', () => { rerender(); - expect(mockUpdateUrlParam).toHaveBeenCalledWith(null); + expect(mockUpdateUrlParam).toHaveBeenCalledWith({ + activeTab: 'query', + graphEventId: '', + isOpen: false, + }); }); it('it updates URL when timeline store changes', async () => { diff --git a/x-pack/plugins/security_solution/public/common/hooks/timeline/use_init_timeline_url_param.ts b/x-pack/plugins/security_solution/public/common/hooks/timeline/use_init_timeline_url_param.ts index 8368023627908..cb4bd92d205d7 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/timeline/use_init_timeline_url_param.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/timeline/use_init_timeline_url_param.ts @@ -23,7 +23,7 @@ export const useInitTimelineFromUrlParam = () => { const onInitialize = useCallback( (initialState: TimelineUrl | null) => { - if (initialState != null && initialState.id !== '') { + if (initialState != null) { queryTimelineById({ activeTimelineTab: initialState.activeTab, duplicate: false, diff --git a/x-pack/plugins/security_solution/public/common/hooks/timeline/use_sync_timeline_url_param.ts b/x-pack/plugins/security_solution/public/common/hooks/timeline/use_sync_timeline_url_param.ts index 8b3d7fb680c96..fc2e9b620c314 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/timeline/use_sync_timeline_url_param.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/timeline/use_sync_timeline_url_param.ts @@ -22,15 +22,12 @@ export const useSyncTimelineUrlParam = () => { ); useEffect(() => { - updateUrlParam( - savedObjectId != null - ? { - id: savedObjectId, - isOpen: show, - activeTab, - graphEventId: graphEventId ?? '', - } - : null - ); + const params = { + ...(savedObjectId ? { id: savedObjectId } : {}), + isOpen: show, + activeTab, + graphEventId: graphEventId ?? '', + }; + updateUrlParam(params); }, [activeTab, graphEventId, savedObjectId, show, updateUrlParam]); }; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_resolve_conflict.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_resolve_conflict.tsx index d0082f858ca12..40eace1bd6e6e 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_resolve_conflict.tsx +++ b/x-pack/plugins/security_solution/public/common/hooks/use_resolve_conflict.tsx @@ -36,13 +36,6 @@ export const useResolveConflict = () => { const getLegacyUrlConflictCallout = useCallback(() => { // This function returns a callout component *if* we have encountered a "legacy URL conflict" scenario - if ( - !spaces || - resolveTimelineConfig?.outcome !== 'conflict' || - resolveTimelineConfig?.alias_target_id == null - ) { - return null; - } const searchQuery = new URLSearchParams(search); const timelineRison = searchQuery.get(URL_PARAM_KEY.timeline) ?? undefined; @@ -59,6 +52,14 @@ export const useResolveConflict = () => { // We have resolved to one object, but another object has a legacy URL alias associated with this ID/page. We should display a // callout with a warning for the user, and provide a way for them to navigate to the other object. const currentObjectId = timelineSearch?.id; + if ( + !spaces || + resolveTimelineConfig?.outcome !== 'conflict' || + resolveTimelineConfig?.alias_target_id == null || + currentObjectId == null + ) { + return null; + } const newSavedObjectId = resolveTimelineConfig?.alias_target_id ?? ''; // This is always defined if outcome === 'conflict' const newTimelineSearch: TimelineUrl = { diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx index 426549123ef10..41479adb42623 100644 --- a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx +++ b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx @@ -157,6 +157,16 @@ describe('Dashboards landing', () => { expect(screen.queryByTestId('dashboardsTable')).not.toBeInTheDocument(); }); + it('should not render loading icon if no read capability', async () => { + mockUseCapabilities.mockReturnValue({ + ...DEFAULT_DASHBOARD_CAPABILITIES, + show: false, + }); + await renderDashboardLanding(); + + expect(screen.queryByTestId('dashboardLoadingIcon')).not.toBeInTheDocument(); + }); + describe('Create Security Dashboard button', () => { it('should render', async () => { await renderDashboardLanding(); diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx index a843533400cf7..e72e38429f35d 100644 --- a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx +++ b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx @@ -132,7 +132,7 @@ export const DashboardsLandingPage = () => { /> - {canReadDashboard && securityTagsExist && initialFilter ? ( + {canReadDashboard && securityTagsExist && initialFilter && ( <> @@ -148,8 +148,11 @@ export const DashboardsLandingPage = () => { urlStateEnabled={false} /> - ) : ( - } /> + )} + {canReadDashboard && !securityTagsExist && ( + } + /> )} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx index 55931e74ff9d4..c4d50062675d2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/index.tsx @@ -25,7 +25,7 @@ import { isEqual } from 'lodash'; import * as i18n from './translations'; import { usePreviewRoute } from './use_preview_route'; import { PreviewHistogram } from './preview_histogram'; -import { PreviewLogsComponent } from './preview_logs'; +import { PreviewLogs } from './preview_logs'; import { useKibana } from '../../../../common/lib/kibana'; import { LoadingHistogram } from './loading_histogram'; import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction'; @@ -274,7 +274,7 @@ const RulePreviewComponent: React.FC = ({ timeframeOptions={previewData.timeframeOptions} /> )} - + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx index 89ed3aaf97b91..24f6b82383a13 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx @@ -69,7 +69,7 @@ interface PreviewHistogramProps { const DEFAULT_HISTOGRAM_HEIGHT = 300; -export const PreviewHistogram = ({ +const PreviewHistogramComponent = ({ previewId, addNoiseWarning, spaceId, @@ -262,3 +262,6 @@ export const PreviewHistogram = ({ ); }; + +export const PreviewHistogram = React.memo(PreviewHistogramComponent); +PreviewHistogram.displayName = 'PreviewHistogram'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_logs.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_logs.tsx index b6f9135c8b99f..1c01a0491e92c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_logs.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_logs.tsx @@ -10,7 +10,7 @@ import { EuiCallOut, EuiText, EuiSpacer, EuiAccordion } from '@elastic/eui'; import type { RulePreviewLogs } from '../../../../../common/api/detection_engine'; import * as i18n from './translations'; -interface PreviewLogsComponentProps { +interface PreviewLogsProps { logs: RulePreviewLogs[]; hasNoiseWarning: boolean; isAborted: boolean; @@ -42,11 +42,7 @@ const addLogs = ( allLogs: SortedLogs[] ) => (logs.length ? [{ startedAt, logs, duration }, ...allLogs] : allLogs); -export const PreviewLogsComponent: React.FC = ({ - logs, - hasNoiseWarning, - isAborted, -}) => { +const PreviewLogsComponent: React.FC = ({ logs, hasNoiseWarning, isAborted }) => { const sortedLogs = useMemo( () => logs.reduce<{ @@ -73,6 +69,9 @@ export const PreviewLogsComponent: React.FC = ({ ); }; +export const PreviewLogs = React.memo(PreviewLogsComponent); +PreviewLogs.displayName = 'PreviewLogs'; + const LogAccordion: React.FC = ({ logs, isError, children }) => { const firstLog = logs[0]; if (!(children || firstLog)) return null; diff --git a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_assistant.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_assistant.test.tsx index 1a538a2ceaeae..b5542b5cac5cf 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_assistant.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_assistant.test.tsx @@ -54,4 +54,28 @@ describe('useAssistant', () => { expect(hookResult.result.current.showAssistant).toEqual(false); expect(hookResult.result.current.promptContextId).toEqual(''); }); + + it('returns anonymized prompt context data', async () => { + jest + .mocked(useAssistantAvailability) + .mockReturnValue({ hasAssistantPrivilege: true, isAssistantEnabled: true }); + jest + .mocked(useAssistantOverlay) + .mockReturnValue({ showAssistantOverlay: jest.fn, promptContextId: '123' }); + + hookResult = renderUseAssistant(); + + const getPromptContext = (useAssistantOverlay as jest.Mock).mock.calls[0][3]; + + expect(await getPromptContext()).toEqual({ + '@timestamp': ['2023-01-01T01:01:01.000Z'], + 'kibana.alert.ancestors.id': ['ancestors-id'], + 'kibana.alert.rule.description': ['rule-description'], + 'kibana.alert.rule.name': ['rule-name'], + 'kibana.alert.rule.parameters.index': ['rule-parameters-index'], + 'kibana.alert.rule.uuid': ['rule-uuid'], + 'kibana.alert.workflow_status': ['open'], + 'process.entity_id': ['process-entity_id'], + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_assistant.ts b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_assistant.ts index 13392692e6746..8d9360fdb0f5e 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_assistant.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_assistant.ts @@ -9,7 +9,7 @@ import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; import { useAssistantOverlay } from '@kbn/elastic-assistant'; import { useCallback } from 'react'; import { useAssistantAvailability } from '../../../assistant/use_assistant_availability'; -import { getPromptContextFromEventDetailsItem } from '../../../assistant/helpers'; +import { getRawData } from '../../../assistant/helpers'; import { ALERT_SUMMARY_CONTEXT_DESCRIPTION, ALERT_SUMMARY_CONVERSATION_ID, @@ -59,7 +59,7 @@ export const useAssistant = ({ const { hasAssistantPrivilege } = useAssistantAvailability(); const useAssistantHook = hasAssistantPrivilege ? useAssistantOverlay : useAssistantNoop; const getPromptContext = useCallback( - async () => getPromptContextFromEventDetailsItem(dataFormattedForFieldBrowser ?? []), + async () => getRawData(dataFormattedForFieldBrowser ?? []), [dataFormattedForFieldBrowser] ); const { promptContextId } = useAssistantHook( diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index 6cd332c23a311..ca43ba15dbf31 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -314,7 +314,7 @@ export interface QueryTimelineById { activeTimelineTab?: TimelineTabs; duplicate?: boolean; graphEventId?: string; - timelineId: string; + timelineId?: string; timelineType?: TimelineType; onError?: TimelineErrorCallback; onOpenTimeline?: (timeline: TimelineModel) => void; @@ -342,55 +342,73 @@ export const queryTimelineById = ({ updateTimeline, }: QueryTimelineById) => { updateIsLoading({ id: TimelineId.active, isLoading: true }); - Promise.resolve(resolveTimeline(timelineId)) - .then((result) => { - const data: SingleTimelineResolveResponse['data'] | null = getOr(null, 'data', result); - if (!data) return; - - const timelineToOpen = omitTypenameInTimeline(data.timeline); - - const { timeline, notes } = formatTimelineResultToModel( - timelineToOpen, - duplicate, - timelineType - ); - - if (onOpenTimeline != null) { - onOpenTimeline(timeline); - } else if (updateTimeline) { - const { from, to } = normalizeTimeRange({ - from: getOr(null, 'dateRange.start', timeline), - to: getOr(null, 'dateRange.end', timeline), - }); - updateTimeline({ + if (timelineId == null) { + updateTimeline({ + id: TimelineId.active, + duplicate: false, + notes: [], + from: DEFAULT_FROM_MOMENT.toISOString(), + to: DEFAULT_TO_MOMENT.toISOString(), + timeline: { + ...timelineDefaults, + id: TimelineId.active, + activeTab: activeTimelineTab, + show: openTimeline, + initialized: true, + }, + })(); + updateIsLoading({ id: TimelineId.active, isLoading: false }); + } else { + Promise.resolve(resolveTimeline(timelineId)) + .then((result) => { + const data: SingleTimelineResolveResponse['data'] | null = getOr(null, 'data', result); + if (!data) return; + + const timelineToOpen = omitTypenameInTimeline(data.timeline); + + const { timeline, notes } = formatTimelineResultToModel( + timelineToOpen, duplicate, - from, - id: TimelineId.active, - notes, - resolveTimelineConfig: { - outcome: data.outcome, - alias_target_id: data.alias_target_id, - alias_purpose: data.alias_purpose, - }, - timeline: { - ...timeline, - activeTab: activeTimelineTab, - graphEventId, - show: openTimeline, - dateRange: { start: from, end: to }, - }, - to, - })(); - } - }) - .catch((error) => { - if (onError != null) { - onError(error, timelineId); - } - }) - .finally(() => { - updateIsLoading({ id: TimelineId.active, isLoading: false }); - }); + timelineType + ); + + if (onOpenTimeline != null) { + onOpenTimeline(timeline); + } else if (updateTimeline) { + const { from, to } = normalizeTimeRange({ + from: getOr(null, 'dateRange.start', timeline), + to: getOr(null, 'dateRange.end', timeline), + }); + updateTimeline({ + duplicate, + from, + id: TimelineId.active, + notes, + resolveTimelineConfig: { + outcome: data.outcome, + alias_target_id: data.alias_target_id, + alias_purpose: data.alias_purpose, + }, + timeline: { + ...timeline, + activeTab: activeTimelineTab, + graphEventId, + show: openTimeline, + dateRange: { start: from, end: to }, + }, + to, + })(); + } + }) + .catch((error) => { + if (onError != null) { + onError(error, timelineId); + } + }) + .finally(() => { + updateIsLoading({ id: TimelineId.active, isLoading: false }); + }); + } }; export const dispatchUpdateTimeline = diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index f29dabf065f21..1bbeb32ae0e87 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -77,6 +77,7 @@ const StatefulTimelineComponent: React.FC = ({ timelineType, description, sessionViewConfig, + initialized, } = useDeepEqualSelector((state) => pick( [ @@ -87,6 +88,7 @@ const StatefulTimelineComponent: React.FC = ({ 'timelineType', 'description', 'sessionViewConfig', + 'initialized', ], getTimeline(state, timelineId) ?? timelineDefaults ) @@ -95,7 +97,7 @@ const StatefulTimelineComponent: React.FC = ({ const { timelineFullScreen } = useTimelineFullScreen(); useEffect(() => { - if (!savedObjectId) { + if (!savedObjectId && !initialized) { dispatch( timelineActions.createTimeline({ id: timelineId, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index 630fb88fe0717..e6707f239f399 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -278,7 +278,7 @@ export const QueryTabContentComponent: React.FC = ({ id: timelineId, }) ); - }, [activeFilterManager, currentTimeline, dispatch, filterManager, timelineId, uiSettings]); + }, [dispatch, filterManager, timelineId]); const [isQueryLoading, { events, inspect, totalCount, pageInfo, loadPage, updatedAt, refetch }] = useTimelineEvents({ diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index 9cebc52da54fa..ca890b9ac3ec4 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -140,7 +140,7 @@ export const addTimelineToStore = ({ ...timeline, filterManager: timelineById[id].filterManager, isLoading: timelineById[id].isLoading, - initialized: timelineById[id].initialized, + initialized: timeline.initialized ?? timelineById[id].initialized, resolveTimelineConfig, dateRange: timeline.status === TimelineStatus.immutable && diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index 0f2939309344f..34cf8b6dcaadf 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -191,7 +191,7 @@ export type SubsetTimelineModel = Readonly< export interface TimelineUrl { activeTab?: TimelineTabs; - id: string; + id?: string; isOpen: boolean; graphEventId?: string; } diff --git a/x-pack/plugins/security_solution_serverless/kibana.jsonc b/x-pack/plugins/security_solution_serverless/kibana.jsonc index 68b6eb71af8d5..3f9080adc5173 100644 --- a/x-pack/plugins/security_solution_serverless/kibana.jsonc +++ b/x-pack/plugins/security_solution_serverless/kibana.jsonc @@ -25,6 +25,9 @@ "optionalPlugins": [ "securitySolutionEss" ], - "requiredBundles": ["kibanaUtils"] + "requiredBundles": [ + "kibanaUtils", + "usageCollection" + ] } } diff --git a/x-pack/plugins/security_solution_serverless/public/pages/assets.tsx b/x-pack/plugins/security_solution_serverless/public/pages/assets.tsx index 1781a52769297..ae3b90c0ec877 100644 --- a/x-pack/plugins/security_solution_serverless/public/pages/assets.tsx +++ b/x-pack/plugins/security_solution_serverless/public/pages/assets.tsx @@ -12,6 +12,7 @@ import { SecurityPageName } from '@kbn/security-solution-navigation'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { EuiCallOut, EuiPageHeader, EuiSpacer, useEuiTheme } from '@elastic/eui'; import { LinkButton } from '@kbn/security-solution-navigation/links'; +import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import { useNavLink } from '../common/hooks/use_nav_links'; import { ExternalPageName } from '../navigation/links/constants'; @@ -42,23 +43,25 @@ export const AssetsRoute: React.FC = () => { return ( - - - - - - - -

{INTEGRATIONS_CALLOUT_DESCRIPTION}

- - {INTEGRATIONS_CALLOUT_BUTTON_TEXT} - -
+ + + + + + + + +

{INTEGRATIONS_CALLOUT_DESCRIPTION}

+ + {INTEGRATIONS_CALLOUT_BUTTON_TEXT} + +
+
); diff --git a/x-pack/plugins/security_solution_serverless/public/pages/investigations.tsx b/x-pack/plugins/security_solution_serverless/public/pages/investigations.tsx index 0aeedb8133a58..af217b94e8998 100644 --- a/x-pack/plugins/security_solution_serverless/public/pages/investigations.tsx +++ b/x-pack/plugins/security_solution_serverless/public/pages/investigations.tsx @@ -10,6 +10,7 @@ import { LandingLinksIcons } from '@kbn/security-solution-navigation/landing_lin import { SecurityPageName } from '@kbn/security-solution-navigation'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { EuiPageHeader, EuiSpacer } from '@elastic/eui'; +import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import { useNavLink } from '../common/hooks/use_nav_links'; export const InvestigationsRoute: React.FC = () => { @@ -19,10 +20,12 @@ export const InvestigationsRoute: React.FC = () => { return ( - - - - + + + + + + ); diff --git a/x-pack/plugins/security_solution_serverless/public/pages/machine_learning.tsx b/x-pack/plugins/security_solution_serverless/public/pages/machine_learning.tsx index 84d5841d14a07..a790e7784e37e 100644 --- a/x-pack/plugins/security_solution_serverless/public/pages/machine_learning.tsx +++ b/x-pack/plugins/security_solution_serverless/public/pages/machine_learning.tsx @@ -10,6 +10,7 @@ import { LandingLinksIconsCategories } from '@kbn/security-solution-navigation/l import { SecurityPageName } from '@kbn/security-solution-navigation'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { EuiPageHeader, EuiSpacer } from '@elastic/eui'; +import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import { useNavLink } from '../common/hooks/use_nav_links'; export const MachineLearningRoute: React.FC = () => { @@ -19,10 +20,12 @@ export const MachineLearningRoute: React.FC = () => { return ( - - - - + + + + + + ); diff --git a/x-pack/plugins/security_solution_serverless/public/pages/project_settings.tsx b/x-pack/plugins/security_solution_serverless/public/pages/project_settings.tsx index dc9e81b819411..9c3f8e5012a67 100644 --- a/x-pack/plugins/security_solution_serverless/public/pages/project_settings.tsx +++ b/x-pack/plugins/security_solution_serverless/public/pages/project_settings.tsx @@ -18,6 +18,7 @@ import { SecurityPageName, } from '@kbn/security-solution-navigation'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import { useNavLink } from '../common/hooks/use_nav_links'; export const ProjectSettingsRoute: React.FC = () => { @@ -34,13 +35,15 @@ export const ProjectSettingsRoute: React.FC = () => { return ( - - - - - - - + + + + + + + + + ); diff --git a/x-pack/plugins/security_solution_serverless/tsconfig.json b/x-pack/plugins/security_solution_serverless/tsconfig.json index ba8d4bbd9688f..6ea4adba54291 100644 --- a/x-pack/plugins/security_solution_serverless/tsconfig.json +++ b/x-pack/plugins/security_solution_serverless/tsconfig.json @@ -37,6 +37,7 @@ "@kbn/cloud-plugin", "@kbn/cloud-security-posture-plugin", "@kbn/fleet-plugin", - "@kbn/core-elasticsearch-server" + "@kbn/core-elasticsearch-server", + "@kbn/usage-collection-plugin" ] } diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/restore_list/restore_table/restore_table.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/restore_list/restore_table/restore_table.tsx index b94e3f631fb16..edb097cecb796 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/restore_list/restore_table/restore_table.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/restore_list/restore_table/restore_table.tsx @@ -8,8 +8,13 @@ import React, { useState, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { orderBy } from 'lodash'; -import { EuiBasicTable, EuiButtonIcon, EuiHealth } from '@elastic/eui'; -import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiButtonIcon, + EuiHealth, + RIGHT_ALIGNMENT, +} from '@elastic/eui'; import { SnapshotRestore } from '../../../../../../common/types'; import { UIM_RESTORE_LIST_EXPAND_INDEX } from '../../../../constants'; @@ -94,7 +99,7 @@ export const RestoreTable: React.FunctionComponent = React.memo(({ restor }, {} as { [key: string]: JSX.Element }); }, [expandedIndices, restores]); - const columns = [ + const columns: Array> = [ { field: 'index', name: i18n.translate('xpack.snapshotRestore.restoreList.table.indexColumnTitle', { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/test_now_mode/simple/ping_list/expanded_row.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/test_now_mode/simple/ping_list/expanded_row.tsx index e3ef6e9cd0cfb..60c80877e7f50 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/test_now_mode/simple/ping_list/expanded_row.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/test_now_mode/simple/ping_list/expanded_row.tsx @@ -5,8 +5,7 @@ * 2.0. */ -// @ts-ignore formatNumber -import { formatNumber } from '@elastic/eui/lib/services/format'; +import { formatNumber } from '@elastic/eui'; import { EuiCallOut, EuiCodeBlock, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_alert_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_alert_list.tsx index 87b33e7bb4933..616f17a2ee685 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_alert_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_alert_list.tsx @@ -10,7 +10,6 @@ import moment, { Duration } from 'moment'; import { padStart, chunk } from 'lodash'; import { EuiBasicTable, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import { AlertStatus, ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils'; import { AlertStatusValues, MaintenanceWindow } from '@kbn/alerting-plugin/common'; import { DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants'; @@ -213,7 +212,7 @@ export const RuleAlertList = (props: RuleAlertListProps) => { }, { field: '', - align: RIGHT_ALIGNMENT, + align: 'right' as const, width: '60px', name: i18n.translate( 'xpack.triggersActionsUI.sections.ruleDetails.alertsList.columns.mute', diff --git a/x-pack/plugins/uptime/public/legacy_uptime/components/monitor/ping_list/expanded_row.tsx b/x-pack/plugins/uptime/public/legacy_uptime/components/monitor/ping_list/expanded_row.tsx index 4335d098eff33..ca4dded961616 100644 --- a/x-pack/plugins/uptime/public/legacy_uptime/components/monitor/ping_list/expanded_row.tsx +++ b/x-pack/plugins/uptime/public/legacy_uptime/components/monitor/ping_list/expanded_row.tsx @@ -5,8 +5,7 @@ * 2.0. */ -// @ts-ignore formatNumber -import { formatNumber } from '@elastic/eui/lib/services/format'; +import { formatNumber } from '@elastic/eui'; import { EuiCallOut, EuiCodeBlock, diff --git a/x-pack/plugins/watcher/__jest__/__snapshots__/license_prompt.test.tsx.snap b/x-pack/plugins/watcher/__jest__/__snapshots__/license_prompt.test.tsx.snap index 7d3768c237574..419a83d62c9b8 100644 --- a/x-pack/plugins/watcher/__jest__/__snapshots__/license_prompt.test.tsx.snap +++ b/x-pack/plugins/watcher/__jest__/__snapshots__/license_prompt.test.tsx.snap @@ -1,80 +1,70 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`License prompt renders a prompt with a link to License Management 1`] = ` - - - - , - ] - } - body={ -

- License error -

- } - iconType="warning" - title={ -

+<_EuiPageEmptyPrompt + actions={ + Array [ + -

- } - /> -
+ , + ] + } + body={ +

+ License error +

+ } + color="danger" + iconType="warning" + title={ +

+ +

+ } +/> `; exports[`License prompt renders a prompt without a link to License Management 1`] = ` - - -

- License error -

-

- -

- - } - iconType="warning" - title={ -

+<_EuiPageEmptyPrompt + actions={ + Array [ + undefined, + ] + } + body={ + +

+ License error +

+

-

- } - /> -
+

+ + } + color="danger" + iconType="warning" + title={ +

+ +

+ } +/> `; diff --git a/x-pack/plugins/watcher/public/application/components/page_error/page_error_forbidden.tsx b/x-pack/plugins/watcher/public/application/components/page_error/page_error_forbidden.tsx index 883f6f1401c7a..12a02318a6c7d 100644 --- a/x-pack/plugins/watcher/public/application/components/page_error/page_error_forbidden.tsx +++ b/x-pack/plugins/watcher/public/application/components/page_error/page_error_forbidden.tsx @@ -7,13 +7,14 @@ import React from 'react'; -import { EuiEmptyPrompt } from '@elastic/eui'; +import { EuiPageTemplate } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; export function PageErrorForbidden() { return ( - ); return ( - - - - - } - body={promptBody} - actions={[promptAction]} - /> - + + + + } + body={promptBody} + actions={[promptAction]} + /> ); }; diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit_page/components/json_watch_edit/json_watch_edit.tsx b/x-pack/plugins/watcher/public/application/sections/watch_edit_page/components/json_watch_edit/json_watch_edit.tsx index fe54e48ce5f1d..66be8a0298bde 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit_page/components/json_watch_edit/json_watch_edit.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit_page/components/json_watch_edit/json_watch_edit.tsx @@ -7,11 +7,7 @@ import React, { useContext, useState } from 'react'; -import { - EuiPageHeader, - EuiSpacer, - EuiPageContentBody_Deprecated as EuiPageContentBody, -} from '@elastic/eui'; +import { EuiPageHeader, EuiSpacer, EuiPageSection } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ExecuteDetails } from '../../../../models/execute_details'; import { getActionType } from '../../../../../../common/lib/get_action_type'; @@ -94,7 +90,7 @@ export const JsonWatchEdit = ({ pageTitle }: { pageTitle: string }) => { ); return ( - + {pageTitle}} bottomBorder @@ -128,6 +124,6 @@ export const JsonWatchEdit = ({ pageTitle }: { pageTitle: string }) => { )} {selectedTab === WATCH_EDIT_TAB && } - + ); }; diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit_page/components/monitoring_watch_edit/monitoring_watch_edit.tsx b/x-pack/plugins/watcher/public/application/sections/watch_edit_page/components/monitoring_watch_edit/monitoring_watch_edit.tsx index efbc56033d12a..d330aa4f41e5d 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit_page/components/monitoring_watch_edit/monitoring_watch_edit.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit_page/components/monitoring_watch_edit/monitoring_watch_edit.tsx @@ -7,7 +7,7 @@ import React, { useContext } from 'react'; -import { EuiPageContent_Deprecated as EuiPageContent, EuiEmptyPrompt, EuiLink } from '@elastic/eui'; +import { EuiLink, EuiPageTemplate } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public'; import { WatchContext } from '../../watch_context'; @@ -28,20 +28,18 @@ export const MonitoringWatchEdit = ({ pageTitle }: { pageTitle: string }) => { ); return ( - - {pageTitle}} - body={

{systemWatchMessage}

} - actions={[ - - - , - ]} - /> -
+ {pageTitle}} + body={

{systemWatchMessage}

} + actions={[ + + + , + ]} + /> ); }; diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit_page/components/threshold_watch_edit/threshold_watch_edit.tsx b/x-pack/plugins/watcher/public/application/sections/watch_edit_page/components/threshold_watch_edit/threshold_watch_edit.tsx index 2934cc67cb8a0..9ebb0d8170fbe 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit_page/components/threshold_watch_edit/threshold_watch_edit.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit_page/components/threshold_watch_edit/threshold_watch_edit.tsx @@ -25,7 +25,7 @@ import { EuiText, EuiTitle, EuiPageHeader, - EuiPageContentBody_Deprecated as EuiPageContentBody, + EuiPageSection, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -236,7 +236,7 @@ export const ThresholdWatchEdit = ({ pageTitle }: { pageTitle: string }) => { }; return ( - + {pageTitle}} description={watch.titleDescription} @@ -953,6 +953,6 @@ export const ThresholdWatchEdit = ({ pageTitle }: { pageTitle: string }) => { close={() => setIsRequestVisible(false)} /> ) : null} - + ); }; diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit_page/watch_edit_page.tsx b/x-pack/plugins/watcher/public/application/sections/watch_edit_page/watch_edit_page.tsx index 1c543d167d642..e2adfe25ea197 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit_page/watch_edit_page.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit_page/watch_edit_page.tsx @@ -8,10 +8,10 @@ import React, { useEffect, useReducer } from 'react'; import { isEqual } from 'lodash'; -import { EuiPageContent_Deprecated as EuiPageContent } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiPageTemplate } from '@elastic/eui'; import { WATCH_TYPES } from '../../../../common/constants'; import { BaseWatch } from '../../../../common/types/watch_types'; import { getPageErrorCode, PageError, SectionLoading } from '../../components'; @@ -133,11 +133,7 @@ export const WatchEditPage = ({ const errorCode = getPageErrorCode(loadError); if (errorCode) { - return ( - - - - ); + return ; } else if (loadError) { return ( + - + ); } diff --git a/x-pack/plugins/watcher/public/application/sections/watch_list_page/watch_list_page.tsx b/x-pack/plugins/watcher/public/application/sections/watch_list_page/watch_list_page.tsx index d2a60fcce5f28..76a8328fb532a 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_list_page/watch_list_page.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_list_page/watch_list_page.tsx @@ -11,20 +11,19 @@ import { CriteriaWithPagination, EuiButton, EuiButtonEmpty, + EuiCallOut, EuiInMemoryTable, EuiIcon, EuiLink, - EuiPageContent_Deprecated as EuiPageContent, - EuiCallOut, EuiSpacer, EuiText, EuiToolTip, - EuiEmptyPrompt, EuiButtonIcon, EuiPopover, EuiContextMenuPanel, EuiContextMenuItem, EuiPageHeader, + EuiPageTemplate, EuiSearchBarOnChangeArgs, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -178,24 +177,20 @@ export const WatchListPage = () => { if (isWatchesLoading) { return ( - + - +
); } const errorCode = getPageErrorCode(error); if (errorCode) { - return ( - - - - ); + return ; } else if (error) { return ( { ); return ( - - - - - } - body={emptyPromptBody} - actions={createWatchContextMenu} - data-test-subj="emptyPrompt" - /> - + + + + } + body={emptyPromptBody} + actions={createWatchContextMenu} + data-test-subj="emptyPrompt" + /> ); } diff --git a/x-pack/plugins/watcher/public/application/sections/watch_status_page/watch_status_page.tsx b/x-pack/plugins/watcher/public/application/sections/watch_status_page/watch_status_page.tsx index f20e4cae114e2..fd423a4f00788 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_status_page/watch_status_page.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_status_page/watch_status_page.tsx @@ -7,12 +7,12 @@ import React, { useEffect, useState } from 'react'; import { - EuiPageContent_Deprecated as EuiPageContent, EuiSpacer, EuiToolTip, EuiBadge, EuiButtonEmpty, EuiPageHeader, + EuiPageTemplate, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -77,23 +77,19 @@ export const WatchStatusPage = ({ if (isWatchDetailLoading) { return ( - + - + ); } if (errorCode) { - return ( - - - - ); + return ; } if (watchDetail) { diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts index 3c044250b00bf..c3d3406f61eb2 100644 --- a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts @@ -6,66 +6,28 @@ */ import expect from '@kbn/expect'; -import { registerEsHelpers } from './lib'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { IngestPutPipelineRequest } from '@elastic/elasticsearch/lib/api/types'; -const API_BASE_PATH = '/api/ingest_pipelines'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - - const { createPipeline, deletePipeline, cleanupPipelines, createIndex, deleteIndex } = - registerEsHelpers(getService); + const ingestPipelines = getService('ingestPipelines'); + const log = getService('log'); describe('Pipelines', function () { after(async () => { - await cleanupPipelines(); + await ingestPipelines.api.deletePipelines(); }); describe('Create', () => { - const PIPELINE_ID = 'test_create_pipeline'; - const REQUIRED_FIELDS_PIPELINE_ID = 'test_create_required_fields_pipeline'; - - after(async () => { - // Clean up any pipelines created in test cases - await Promise.all([PIPELINE_ID, REQUIRED_FIELDS_PIPELINE_ID].map(deletePipeline)).catch( - (err) => { - // eslint-disable-next-line no-console - console.log(`[Cleanup error] Error deleting pipelines: ${err.message}`); - throw err; - } - ); - }); - it('should create a pipeline', async () => { + const pipelineRequestBody = ingestPipelines.fixtures.createPipelineBody(); const { body } = await supertest - .post(API_BASE_PATH) + .post(ingestPipelines.fixtures.apiBasePath) .set('kbn-xsrf', 'xxx') - .send({ - name: PIPELINE_ID, - description: 'test pipeline description', - processors: [ - { - script: { - source: 'ctx._type = null', - }, - }, - ], - on_failure: [ - { - set: { - field: 'error.message', - value: '{{ failure_message }}', - }, - }, - ], - version: 1, - _meta: { - field_1: 'test', - field_2: 10, - }, - }) + .send(pipelineRequestBody) .expect(200); expect(body).to.eql({ @@ -74,20 +36,12 @@ export default function ({ getService }: FtrProviderContext) { }); it('should create a pipeline with only required fields', async () => { + const pipelineRequestBody = ingestPipelines.fixtures.createPipelineBodyWithRequiredFields(); // Includes name and processors[] only + const { body } = await supertest - .post(API_BASE_PATH) + .post(ingestPipelines.fixtures.apiBasePath) .set('kbn-xsrf', 'xxx') - // Excludes description, version, on_failure processors, and _meta - .send({ - name: REQUIRED_FIELDS_PIPELINE_ID, - processors: [ - { - script: { - source: 'ctx._type = null', - }, - }, - ], - }) + .send(pipelineRequestBody) .expect(200); expect(body).to.eql({ @@ -96,80 +50,56 @@ export default function ({ getService }: FtrProviderContext) { }); it('should not allow creation of an existing pipeline', async () => { + const pipelineRequestBody = ingestPipelines.fixtures.createPipelineBodyWithRequiredFields(); // Includes name and processors[] only + + const { name, ...esPipelineRequestBody } = pipelineRequestBody; + + // First, create a pipeline using the ES API + await ingestPipelines.api.createPipeline({ id: name, ...esPipelineRequestBody }); + + // Then, create a pipeline with our internal API const { body } = await supertest - .post(API_BASE_PATH) + .post(ingestPipelines.fixtures.apiBasePath) .set('kbn-xsrf', 'xxx') - .send({ - name: PIPELINE_ID, - description: 'test pipeline description', - processors: [ - { - script: { - source: 'ctx._type = null', - }, - }, - ], - version: 1, - _meta: { - field_1: 'test', - field_2: 10, - }, - }) + .send(pipelineRequestBody) .expect(409); expect(body).to.eql({ statusCode: 409, error: 'Conflict', - message: `There is already a pipeline with name '${PIPELINE_ID}'.`, + message: `There is already a pipeline with name '${name}'.`, }); }); }); describe('Update', () => { - const PIPELINE_ID = 'test_update_pipeline'; - const PIPELINE = { - description: 'test pipeline description', - processors: [ - { - script: { - source: 'ctx._type = null', - }, - }, - ], - version: 1, - on_failure: [ - { - set: { - field: '_index', - value: 'failed-{{ _index }}', - }, - }, - ], - _meta: { - field_1: 'test', - field_2: 10, - }, - }; + let pipeline: Omit; + let pipelineName: string; before(async () => { // Create pipeline that can be used to test PUT request try { - await createPipeline({ body: PIPELINE, id: PIPELINE_ID }, true); + const pipelineRequestBody = + ingestPipelines.fixtures.createPipelineBodyWithRequiredFields(); + const { name, ...esPipelineRequestBody } = pipelineRequestBody; + + pipeline = esPipelineRequestBody; + pipelineName = name; + await ingestPipelines.api.createPipeline({ id: name, ...esPipelineRequestBody }); } catch (err) { - // eslint-disable-next-line no-console - console.log('[Setup error] Error creating ingest pipeline'); + log.debug('[Setup error] Error creating ingest pipeline'); throw err; } }); it('should allow an existing pipeline to be updated', async () => { - const uri = `${API_BASE_PATH}/${PIPELINE_ID}`; + const uri = `${ingestPipelines.fixtures.apiBasePath}/${pipelineName}`; const { body } = await supertest .put(uri) .set('kbn-xsrf', 'xxx') .send({ - ...PIPELINE, + ...pipeline, description: 'updated test pipeline description', _meta: { field_1: 'updated', @@ -184,14 +114,14 @@ export default function ({ getService }: FtrProviderContext) { }); it('should allow optional fields to be removed', async () => { - const uri = `${API_BASE_PATH}/${PIPELINE_ID}`; + const uri = `${ingestPipelines.fixtures.apiBasePath}/${pipelineName}`; const { body } = await supertest .put(uri) .set('kbn-xsrf', 'xxx') .send({ - processors: PIPELINE.processors, // removes description, version, on_failure, and _meta + processors: pipeline.processors, }) .expect(200); @@ -201,13 +131,13 @@ export default function ({ getService }: FtrProviderContext) { }); it('should not allow a non-existing pipeline to be updated', async () => { - const uri = `${API_BASE_PATH}/pipeline_does_not_exist`; + const uri = `${ingestPipelines.fixtures.apiBasePath}/pipeline_does_not_exist`; const { body } = await supertest .put(uri) .set('kbn-xsrf', 'xxx') .send({ - ...PIPELINE, + ...pipeline, description: 'updated test pipeline description', _meta: { field_1: 'updated', @@ -226,118 +156,100 @@ export default function ({ getService }: FtrProviderContext) { }); describe('Get', () => { - const PIPELINE_ID = 'test_get_pipeline'; - const PIPELINE = { - description: 'test pipeline description', - processors: [ - { - script: { - source: 'ctx._type = null', - }, - }, - ], - version: 1, - _meta: { - field_1: 'test', - field_2: 10, - }, - }; + let pipeline: Omit; + let pipelineName: string; before(async () => { // Create pipeline that can be used to test GET request try { - await createPipeline({ body: PIPELINE, id: PIPELINE_ID }, true); + const pipelineRequestBody = + ingestPipelines.fixtures.createPipelineBodyWithRequiredFields(); + const { name, ...esPipelineRequestBody } = pipelineRequestBody; + + pipeline = esPipelineRequestBody; + pipelineName = name; + await ingestPipelines.api.createPipeline({ id: name, ...esPipelineRequestBody }); } catch (err) { - // eslint-disable-next-line no-console - console.log('[Setup error] Error creating ingest pipeline'); + log.debug('[Setup error] Error creating ingest pipeline'); throw err; } }); describe('all pipelines', () => { it('should return an array of pipelines', async () => { - const { body } = await supertest.get(API_BASE_PATH).set('kbn-xsrf', 'xxx').expect(200); + const { body } = await supertest + .get(ingestPipelines.fixtures.apiBasePath) + .set('kbn-xsrf', 'xxx') + .expect(200); expect(Array.isArray(body)).to.be(true); // There are some pipelines created OOTB with ES // To not be dependent on these, we only confirm the pipeline we created as part of the test exists - const testPipeline = body.find(({ name }: { name: string }) => name === PIPELINE_ID); + const testPipeline = body.find(({ name }: { name: string }) => name === pipelineName); expect(testPipeline).to.eql({ - ...PIPELINE, + ...pipeline, isManaged: false, - name: PIPELINE_ID, + name: pipelineName, }); }); }); describe('one pipeline', () => { it('should return a single pipeline', async () => { - const uri = `${API_BASE_PATH}/${PIPELINE_ID}`; + const uri = `${ingestPipelines.fixtures.apiBasePath}/${pipelineName}`; const { body } = await supertest.get(uri).set('kbn-xsrf', 'xxx').expect(200); expect(body).to.eql({ - ...PIPELINE, + ...pipeline, isManaged: false, - name: PIPELINE_ID, + name: pipelineName, }); }); }); }); describe('Delete', () => { - const PIPELINE = { - description: 'test pipeline description', - processors: [ - { - script: { - source: 'ctx._type = null', - }, - }, - ], - version: 1, - _meta: { - field_1: 'test', - field_2: 10, - }, - }; - - const pipelineA = { body: PIPELINE, id: 'test_delete_pipeline_a' }; - const pipelineB = { body: PIPELINE, id: 'test_delete_pipeline_b' }; - const pipelineC = { body: PIPELINE, id: 'test_delete_pipeline_c' }; - const pipelineD = { body: PIPELINE, id: 'test_delete_pipeline_d' }; + const pipelineIds: string[] = []; before(async () => { + const pipelineA = ingestPipelines.fixtures.createPipelineBodyWithRequiredFields(); + const pipelineB = ingestPipelines.fixtures.createPipelineBodyWithRequiredFields(); + const pipelineC = ingestPipelines.fixtures.createPipelineBodyWithRequiredFields(); + const pipelineD = ingestPipelines.fixtures.createPipelineBodyWithRequiredFields(); + // Create several pipelines that can be used to test deletion await Promise.all( - [pipelineA, pipelineB, pipelineC, pipelineD].map((pipeline) => createPipeline(pipeline)) + [pipelineA, pipelineB, pipelineC, pipelineD].map((pipeline) => { + const { name, ...pipelineRequestBody } = pipeline; + pipelineIds.push(pipeline.name); + return ingestPipelines.api.createPipeline({ id: name, ...pipelineRequestBody }); + }) ).catch((err) => { - // eslint-disable-next-line no-console - console.log(`[Setup error] Error creating pipelines: ${err.message}`); + log.debug(`[Setup error] Error creating pipelines: ${err.message}`); throw err; }); }); it('should delete a pipeline', async () => { - const { id } = pipelineA; + const pipelineA = pipelineIds[0]; - const uri = `${API_BASE_PATH}/${id}`; + const uri = `${ingestPipelines.fixtures.apiBasePath}/${pipelineA}`; const { body } = await supertest.delete(uri).set('kbn-xsrf', 'xxx').expect(200); expect(body).to.eql({ - itemsDeleted: [id], + itemsDeleted: [pipelineA], errors: [], }); }); it('should delete multiple pipelines', async () => { - const { id: pipelineBId } = pipelineB; - const { id: pipelineCId } = pipelineC; - - const uri = `${API_BASE_PATH}/${pipelineBId},${pipelineCId}`; + const pipelineB = pipelineIds[1]; + const pipelineC = pipelineIds[2]; + const uri = `${ingestPipelines.fixtures.apiBasePath}/${pipelineIds[1]},${pipelineIds[2]}`; const { body: { itemsDeleted, errors }, @@ -346,21 +258,21 @@ export default function ({ getService }: FtrProviderContext) { expect(errors).to.eql([]); // The itemsDeleted array order isn't guaranteed, so we assert against each pipeline name instead - [pipelineBId, pipelineCId].forEach((pipelineName) => { + [pipelineB, pipelineC].forEach((pipelineName) => { expect(itemsDeleted.includes(pipelineName)).to.be(true); }); }); it('should return an error for any pipelines not sucessfully deleted', async () => { const PIPELINE_DOES_NOT_EXIST = 'pipeline_does_not_exist'; - const { id: existingPipelineId } = pipelineD; + const pipelineD = pipelineIds[3]; - const uri = `${API_BASE_PATH}/${existingPipelineId},${PIPELINE_DOES_NOT_EXIST}`; + const uri = `${ingestPipelines.fixtures.apiBasePath}/${pipelineD},${PIPELINE_DOES_NOT_EXIST}`; const { body } = await supertest.delete(uri).set('kbn-xsrf', 'xxx').expect(200); expect(body).to.eql({ - itemsDeleted: [existingPipelineId], + itemsDeleted: [pipelineD], errors: [ { name: PIPELINE_DOES_NOT_EXIST, @@ -383,49 +295,14 @@ export default function ({ getService }: FtrProviderContext) { describe('Simulate', () => { it('should successfully simulate a pipeline', async () => { + const { name, ...pipeline } = ingestPipelines.fixtures.createPipelineBody(); + const documents = ingestPipelines.fixtures.createDocuments(); const { body } = await supertest - .post(`${API_BASE_PATH}/simulate`) + .post(`${ingestPipelines.fixtures.apiBasePath}/simulate`) .set('kbn-xsrf', 'xxx') .send({ - pipeline: { - description: 'test simulate pipeline description', - processors: [ - { - set: { - field: 'field2', - value: '_value', - }, - }, - ], - version: 1, - on_failure: [ - { - set: { - field: '_index', - value: 'failed-{{ _index }}', - }, - }, - ], - _meta: { - field: 'test simulate metadata', - }, - }, - documents: [ - { - _index: 'index', - _id: 'id', - _source: { - foo: 'bar', - }, - }, - { - _index: 'index', - _id: 'id', - _source: { - foo: 'rab', - }, - }, - ], + pipeline, + documents, }) .expect(200); @@ -435,36 +312,15 @@ export default function ({ getService }: FtrProviderContext) { }); it('should successfully simulate a pipeline with only required pipeline fields', async () => { + const { name, ...pipeline } = + ingestPipelines.fixtures.createPipelineBodyWithRequiredFields(); + const documents = ingestPipelines.fixtures.createDocuments(); const { body } = await supertest - .post(`${API_BASE_PATH}/simulate`) + .post(`${ingestPipelines.fixtures.apiBasePath}/simulate`) .set('kbn-xsrf', 'xxx') .send({ - pipeline: { - processors: [ - { - set: { - field: 'field2', - value: '_value', - }, - }, - ], - }, - documents: [ - { - _index: 'index', - _id: 'id', - _source: { - foo: 'bar', - }, - }, - { - _index: 'index', - _id: 'id', - _source: { - foo: 'rab', - }, - }, - ], + pipeline, + documents, }) .expect(200); @@ -484,10 +340,9 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { // Create an index with a document that can be used to test GET request try { - await createIndex({ id: DOCUMENT_ID, index: INDEX, body: DOCUMENT }); + await ingestPipelines.api.createIndex({ id: DOCUMENT_ID, index: INDEX, body: DOCUMENT }); } catch (err) { - // eslint-disable-next-line no-console - console.log('[Setup error] Error creating index'); + log.debug('[Setup error] Error creating index'); throw err; } }); @@ -495,16 +350,15 @@ export default function ({ getService }: FtrProviderContext) { after(async () => { // Clean up index created try { - await deleteIndex(INDEX); + await ingestPipelines.api.deleteIndex(INDEX); } catch (err) { - // eslint-disable-next-line no-console - console.log('[Cleanup error] Error deleting index'); + log.debug('[Cleanup error] Error deleting index'); throw err; } }); it('should return a document', async () => { - const uri = `${API_BASE_PATH}/documents/${INDEX}/${DOCUMENT_ID}`; + const uri = `${ingestPipelines.fixtures.apiBasePath}/documents/${INDEX}/${DOCUMENT_ID}`; const { body } = await supertest.get(uri).set('kbn-xsrf', 'xxx').expect(200); @@ -516,7 +370,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should return an error if the document does not exist', async () => { - const uri = `${API_BASE_PATH}/documents/${INDEX}/2`; // Document 2 does not exist + const uri = `${ingestPipelines.fixtures.apiBasePath}/documents/${INDEX}/2`; // Document 2 does not exist const { body } = await supertest.get(uri).set('kbn-xsrf', 'xxx').expect(404); @@ -534,7 +388,7 @@ export default function ({ getService }: FtrProviderContext) { const validCsv = 'source_field,copy_action,format_action,timestamp_format,destination_field,Notes\nsrcip,,,,source.address,Copying srcip to source.address'; const { body } = await supertest - .post(`${API_BASE_PATH}/parse_csv`) + .post(`${ingestPipelines.fixtures.apiBasePath}/parse_csv`) .set('kbn-xsrf', 'xxx') .send({ copyAction: 'copy', diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/api.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/api.ts new file mode 100644 index 0000000000000..1e185b88b7587 --- /dev/null +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/api.ts @@ -0,0 +1,69 @@ +/* + * 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 { IngestPutPipelineRequest } from '@elastic/elasticsearch/lib/api/types'; +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export function IngestPipelinesAPIProvider({ getService }: FtrProviderContext) { + const es = getService('es'); + const retry = getService('retry'); + const log = getService('log'); + + return { + async createPipeline(pipeline: IngestPutPipelineRequest) { + log.debug(`Creating pipeline: '${pipeline.id}'`); + + const createResponse = await es.ingest.putPipeline(pipeline); + expect(createResponse) + .to.have.property('acknowledged') + .eql(true, 'Response for create pipelines should be acknowledged.'); + + await this.waitForPipelinesToExist(pipeline.id, `expected ${pipeline.id} to be created`); + }, + + async waitForPipelinesToExist(pipelineId: string, errorMsg?: string) { + await retry.tryForTime(30 * 1000, async () => { + const pipeline = await es.ingest.getPipeline({ id: pipelineId }); + const pipelineNames = Object.keys(pipeline); + + if (pipelineNames.length === 1 && pipelineNames[0] === pipelineId) { + return true; + } else { + throw new Error(errorMsg || `pipeline '${pipelineId}' should exist`); + } + }); + }, + + async deletePipelines() { + const pipelines = await es.ingest.getPipeline(); + // Assumes all test pipelines will be prefixed with `test-pipeline*` + const pipelineIds = Object.keys(pipelines).filter((pipeline) => + pipeline.includes('test-pipeline') + ); + + const deletePipeline = (pipelineId: string) => es.ingest.deletePipeline({ id: pipelineId }); + + return Promise.all(pipelineIds.map(deletePipeline)).catch((err) => { + log.debug(`[Cleanup error] Error deleting ES resources: ${err.message}`); + }); + }, + + async createIndex(index: { index: string; id: string; body: object }) { + log.debug(`Creating index: '${index.index}'`); + + return await es.index(index); + }, + + async deleteIndex(indexName: string) { + log.debug(`Deleting index: '${indexName}'`); + + return await es.indices.delete({ index: indexName }); + }, + }; +} diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/elasticsearch.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/elasticsearch.ts deleted file mode 100644 index c2a42356f5f51..0000000000000 --- a/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/elasticsearch.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../../../ftr_provider_context'; - -interface Processor { - [key: string]: { - [key: string]: unknown; - }; -} - -interface Pipeline { - id: string; - body: { - description: string; - processors: Processor[]; - version?: number; - }; -} - -/** - * Helpers to create and delete pipelines on the Elasticsearch instance - * during our tests. - * @param {ElasticsearchClient} es The Elasticsearch client instance - */ -export const registerEsHelpers = (getService: FtrProviderContext['getService']) => { - let pipelinesCreated: string[] = []; - - const es = getService('es'); - - const createPipeline = (pipeline: Pipeline, cachePipeline?: boolean) => { - if (cachePipeline) { - pipelinesCreated.push(pipeline.id); - } - - return es.ingest.putPipeline(pipeline); - }; - - const deletePipeline = (pipelineId: string) => es.ingest.deletePipeline({ id: pipelineId }); - - const cleanupPipelines = () => - Promise.all(pipelinesCreated.map(deletePipeline)) - .then(() => { - pipelinesCreated = []; - }) - .catch((err) => { - // eslint-disable-next-line no-console - console.log(`[Cleanup error] Error deleting ES resources: ${err.message}`); - }); - - const createIndex = (index: { index: string; id: string; body: object }) => { - return es.index(index); - }; - - const deleteIndex = (indexName: string) => { - return es.indices.delete({ index: indexName }); - }; - - return { - createPipeline, - deletePipeline, - cleanupPipelines, - createIndex, - deleteIndex, - }; -}; diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/fixtures.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/fixtures.ts new file mode 100644 index 0000000000000..c148101749085 --- /dev/null +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/fixtures.ts @@ -0,0 +1,109 @@ +/* + * 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 { + IngestProcessorContainer, + VersionNumber, + Metadata, + IngestPutPipelineRequest, +} from '@elastic/elasticsearch/lib/api/types'; + +interface Pipeline { + name: string; + description?: string; + onFailureProcessors?: IngestProcessorContainer[]; + processors: IngestProcessorContainer[]; + version?: VersionNumber; + metadata?: Metadata; +} + +interface IngestPutPipelineInternalRequest extends Omit { + name: string; +} + +export function IngestPipelinesFixturesProvider() { + const defaultProcessors: IngestProcessorContainer[] = [ + { + script: { + source: 'ctx._type = null', + }, + }, + ]; + + const defaultOnFailureProcessors: IngestProcessorContainer[] = [ + { + set: { + field: 'error.message', + value: '{{ failure_message }}', + }, + }, + ]; + + const defaultMetadata: Metadata = { + field_1: 'test', + field_2: 10, + }; + + const apiBasePath = '/api/ingest_pipelines'; + + const createPipelineBodyWithRequiredFields = (): IngestPutPipelineInternalRequest => { + return { + name: `test-pipeline-required-fields-${Math.random()}`, + processors: defaultProcessors, + }; + }; + + const createPipelineBody = (pipeline?: Pipeline): IngestPutPipelineInternalRequest => { + if (pipeline) { + const { name, description, processors, onFailureProcessors, version, metadata } = pipeline; + return { + name, + description, + processors, + on_failure: onFailureProcessors, + version, + _meta: metadata, + }; + } + + // Use default payload if none is provided + return { + name: `test-pipeline-${Math.random()}`, + description: 'test pipeline description', + processors: defaultProcessors, + on_failure: defaultOnFailureProcessors, + version: 1, + _meta: defaultMetadata, + }; + }; + + const createDocuments = () => { + return [ + { + _index: 'index', + _id: 'id1', + _source: { + foo: 'bar', + }, + }, + { + _index: 'index', + _id: 'id2', + _source: { + foo: 'rab', + }, + }, + ]; + }; + + return { + createPipelineBodyWithRequiredFields, + createPipelineBody, + createDocuments, + apiBasePath, + }; +} diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/index.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/index.ts index 27a4d9c59cff0..a734e993f7728 100644 --- a/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/index.ts +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/index.ts @@ -5,4 +5,5 @@ * 2.0. */ -export { registerEsHelpers } from './elasticsearch'; +export { IngestPipelinesAPIProvider } from './api'; +export { IngestPipelinesFixturesProvider } from './fixtures'; diff --git a/x-pack/test/api_integration/apis/synthetics/enable_default_alerting.ts b/x-pack/test/api_integration/apis/synthetics/enable_default_alerting.ts index 32ea443da4a09..1d3ed0c6084dc 100644 --- a/x-pack/test/api_integration/apis/synthetics/enable_default_alerting.ts +++ b/x-pack/test/api_integration/apis/synthetics/enable_default_alerting.ts @@ -14,8 +14,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { getFixtureJson } from './helper/get_fixture_json'; export default function ({ getService }: FtrProviderContext) { - // FLAKY: https://github.com/elastic/kibana/issues/158408 - describe.skip('EnableDefaultAlerting', function () { + describe('EnableDefaultAlerting', function () { this.tags('skipCloud'); const supertest = getService('supertest'); @@ -56,6 +55,7 @@ export default function ({ getService }: FtrProviderContext) { 'nextRun', 'lastRun', 'snoozeSchedule', + 'viewInAppRelativeUrl', ]; const statusRule = apiResponse.body.statusRule; @@ -129,6 +129,7 @@ const defaultAlertRules = { lastDuration: 64, }, ruleTypeId: 'xpack.synthetics.alerts.monitorStatus', + viewInAppRelativeUrl: '/app/observability/alerts/rules/574e82f0-1672-11ee-8e7d-c985c0ef6c2e', }, tlsRule: { id: '574eaa00-1672-11ee-8e7d-c985c0ef6c2e', @@ -160,5 +161,6 @@ const defaultAlertRules = { lastDuration: 193, }, ruleTypeId: 'xpack.synthetics.alerts.tls', + viewInAppRelativeUrl: '/app/observability/alerts/rules/574e82f0-1672-11ee-8e7d-c985c0ef6c2e', }, }; diff --git a/x-pack/test/api_integration/services/index.ts b/x-pack/test/api_integration/services/index.ts index d83825349f1e9..6ef3e393a86e6 100644 --- a/x-pack/test/api_integration/services/index.ts +++ b/x-pack/test/api_integration/services/index.ts @@ -20,6 +20,7 @@ import { InfraOpsSourceConfigurationProvider } from './infraops_source_configura import { MachineLearningProvider } from './ml'; import { IngestManagerProvider } from '../../common/services/ingest_manager'; import { TransformProvider } from './transform'; +import { IngestPipelinesProvider } from './ingest_pipelines'; export const services = { ...commonServices, @@ -35,4 +36,5 @@ export const services = { ml: MachineLearningProvider, ingestManager: IngestManagerProvider, transform: TransformProvider, + ingestPipelines: IngestPipelinesProvider, }; diff --git a/x-pack/test/api_integration/services/ingest_pipelines.ts b/x-pack/test/api_integration/services/ingest_pipelines.ts new file mode 100644 index 0000000000000..589ef8135bb7a --- /dev/null +++ b/x-pack/test/api_integration/services/ingest_pipelines.ts @@ -0,0 +1,22 @@ +/* + * 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 { FtrProviderContext } from '../ftr_provider_context'; +import { + IngestPipelinesAPIProvider, + IngestPipelinesFixturesProvider, +} from '../apis/management/ingest_pipelines/lib'; + +export function IngestPipelinesProvider(context: FtrProviderContext) { + const api = IngestPipelinesAPIProvider(context); + const fixtures = IngestPipelinesFixturesProvider(); + + return { + api, + fixtures, + }; +} diff --git a/x-pack/test/functional/apps/aiops/change_point_detection.ts b/x-pack/test/functional/apps/aiops/change_point_detection.ts index 0c55e081afc92..f52e00892acb9 100644 --- a/x-pack/test/functional/apps/aiops/change_point_detection.ts +++ b/x-pack/test/functional/apps/aiops/change_point_detection.ts @@ -16,8 +16,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // aiops lives in the ML UI so we need some related services. const ml = getService('ml'); - // Failing: See https://github.com/elastic/kibana/issues/158851 - describe.skip('change point detection', async function () { + describe('change point detection', async function () { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ecommerce'); await ml.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); @@ -66,7 +65,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await aiops.changePointDetectionPage.selectSplitField(0, 'geoip.city_name'); await aiops.changePointDetectionPage.getTable(0).waitForTableToLoad(); const result = await aiops.changePointDetectionPage.getTable(0).parseTable(); - expect(result.length).to.be(7); + // the aggregation may return different results (+-1) + expect(result.length).to.be.above(5); // assert asc sorting by p_value is applied expect(parseFloat(result[0].pValue)).to.be.lessThan(parseFloat(result[4].pValue)); }); diff --git a/x-pack/test/functional/apps/infra/constants.ts b/x-pack/test/functional/apps/infra/constants.ts index 7add59b59dfda..8297df165a9b7 100644 --- a/x-pack/test/functional/apps/infra/constants.ts +++ b/x-pack/test/functional/apps/infra/constants.ts @@ -45,4 +45,5 @@ export const ML_JOB_IDS = [ export const HOSTS_LINK_LOCAL_STORAGE_KEY = 'inventoryUI:hostsLinkClicked'; +export const NODE_DETAILS_PATH = 'detail/host'; export const HOSTS_VIEW_PATH = 'metrics/hosts'; diff --git a/x-pack/test/functional/apps/infra/hosts_view.ts b/x-pack/test/functional/apps/infra/hosts_view.ts index 11d0a9675573b..c0c425b219637 100644 --- a/x-pack/test/functional/apps/infra/hosts_view.ts +++ b/x-pack/test/functional/apps/infra/hosts_view.ts @@ -93,6 +93,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const security = getService('security'); const testSubjects = getService('testSubjects'); const pageObjects = getPageObjects([ + 'assetDetails', 'common', 'infraHome', 'timePicker', @@ -281,7 +282,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Overview Tab', () => { before(async () => { - await pageObjects.infraHostsView.clickOverviewFlyoutTab(); + await pageObjects.assetDetails.clickOverviewFlyoutTab(); }); [ @@ -292,7 +293,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ].forEach(({ metric, value }) => { it(`${metric} tile should show ${value}`, async () => { await retry.try(async () => { - const tileValue = await pageObjects.infraHostsView.getAssetDetailsKPITileValue( + const tileValue = await pageObjects.assetDetails.getAssetDetailsKPITileValue( metric ); expect(tileValue).to.eql(value); @@ -301,141 +302,67 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should render 8 charts in the Metrics section', async () => { - const hosts = await pageObjects.infraHostsView.getAssetDetailsMetricsCharts(); + const hosts = await pageObjects.assetDetails.getAssetDetailsMetricsCharts(); expect(hosts.length).to.equal(8); }); - it('should navigate to metadata tab', async () => { - await pageObjects.infraHostsView.clickShowAllMetadataOverviewTab(); - await pageObjects.header.waitUntilLoadingHasFinished(); - await pageObjects.infraHostsView.metadataTableExist(); - await pageObjects.infraHostsView.clickOverviewFlyoutTab(); - }); - it('should show alerts', async () => { await pageObjects.header.waitUntilLoadingHasFinished(); - await pageObjects.infraHostsView.overviewAlertsTitleExist(); - }); - - it('should open alerts flyout', async () => { - await pageObjects.header.waitUntilLoadingHasFinished(); - await pageObjects.infraHostsView.clickOverviewOpenAlertsFlyout(); - // There are 2 flyouts open (asset details and alerts) - // so we need a stricter selector - // to be sure that we are closing the alerts flyout - const closeAlertFlyout = await find.byCssSelector( - '[aria-labelledby="flyoutRuleAddTitle"] > [data-test-subj="euiFlyoutCloseButton"]' - ); - await closeAlertFlyout.click(); - }); - - it('should navigate to alerts', async () => { - await pageObjects.infraHostsView.clickOverviewLinkToAlerts(); - await pageObjects.header.waitUntilLoadingHasFinished(); - const url = parse(await browser.getCurrentUrl()); - - const query = decodeURIComponent(url.query ?? ''); - - const alertsQuery = - "_a=(kuery:'host.name:\"Jennys-MBP.fritz.box\"',rangeFrom:'2023-03-28T18:20:00.000Z',rangeTo:'2023-03-28T18:21:00.000Z',status:all)"; - - expect(url.pathname).to.eql('/app/observability/alerts'); - expect(query).to.contain(alertsQuery); - - await returnTo(HOSTS_VIEW_PATH); + await pageObjects.assetDetails.overviewAlertsTitleExists(); }); }); describe('Metadata Tab', () => { before(async () => { - await pageObjects.infraHostsView.clickMetadataFlyoutTab(); + await pageObjects.assetDetails.clickMetadataFlyoutTab(); }); - it('should render metadata tab, add and remove filter', async () => { - await pageObjects.infraHostsView.metadataTableExist(); - - // Add Pin - await pageObjects.infraHostsView.clickAddMetadataPin(); - expect(await pageObjects.infraHostsView.getRemovePinExist()).to.be(true); - - // Persist pin after refresh - await browser.refresh(); - await retry.try(async () => { - await pageObjects.infraHome.waitForLoading(); - const removePinExist = await pageObjects.infraHostsView.getRemovePinExist(); - expect(removePinExist).to.be(true); - }); - - // Remove Pin - await pageObjects.infraHostsView.clickRemoveMetadataPin(); - expect(await pageObjects.infraHostsView.getRemovePinExist()).to.be(false); + it('should show metadata table', async () => { + await pageObjects.assetDetails.metadataTableExists(); + }); - await pageObjects.infraHostsView.clickAddMetadataFilter(); + it('should render metadata tab, add and remove filter', async () => { + // Add Filter + await pageObjects.assetDetails.clickAddMetadataFilter(); await pageObjects.header.waitUntilLoadingHasFinished(); - // Add Filter - const addedFilter = await pageObjects.infraHostsView.getAppliedFilter(); + const addedFilter = await pageObjects.assetDetails.getMetadataAppliedFilter(); expect(addedFilter).to.contain('host.architecture: arm64'); - const removeFilterExists = await pageObjects.infraHostsView.getRemoveFilterExist(); + const removeFilterExists = await pageObjects.assetDetails.metadataRemoveFilterExists(); expect(removeFilterExists).to.be(true); // Remove filter - await pageObjects.infraHostsView.clickRemoveMetadataFilter(); + await pageObjects.assetDetails.clickRemoveMetadataFilter(); await pageObjects.header.waitUntilLoadingHasFinished(); const removeFilterShouldNotExist = - await pageObjects.infraHostsView.getRemoveFilterExist(); + await pageObjects.assetDetails.metadataRemovePinExists(); expect(removeFilterShouldNotExist).to.be(false); }); - - it('should render metadata tab, pin and unpin table row', async () => { - // Add Pin - await pageObjects.infraHostsView.clickAddMetadataPin(); - expect(await pageObjects.infraHostsView.getRemovePinExist()).to.be(true); - - // Persist pin after refresh - await browser.refresh(); - await retry.try(async () => { - await pageObjects.infraHome.waitForLoading(); - const removePinExist = await pageObjects.infraHostsView.getRemovePinExist(); - expect(removePinExist).to.be(true); - }); - - // Remove Pin - await pageObjects.infraHostsView.clickRemoveMetadataPin(); - expect(await pageObjects.infraHostsView.getRemovePinExist()).to.be(false); - }); }); describe('Processes Tab', () => { before(async () => { - await pageObjects.infraHostsView.clickProcessesFlyoutTab(); - }); - it('should render processes tab and with Total Value summary', async () => { - const processesTotalValue = - await pageObjects.infraHostsView.getProcessesTabContentTotalValue(); - const processValue = await processesTotalValue.getVisibleText(); - expect(processValue).to.eql('313'); + await pageObjects.assetDetails.clickProcessesFlyoutTab(); }); - it('should expand processes table row', async () => { - await pageObjects.infraHostsView.getProcessesTable(); - await pageObjects.infraHostsView.getProcessesTableBody(); - await pageObjects.infraHostsView.clickProcessesTableExpandButton(); + it('should show processes table', async () => { + await pageObjects.assetDetails.processesTableExists(); }); }); describe('Logs Tab', () => { before(async () => { - await pageObjects.infraHostsView.clickLogsFlyoutTab(); + await pageObjects.assetDetails.clickLogsFlyoutTab(); }); + it('should render logs tab', async () => { - await testSubjects.existOrFail('infraAssetDetailsLogsTabContent'); + await pageObjects.assetDetails.logsExists(); }); }); describe('Flyout links', () => { it('should navigate to APM services after click', async () => { - await pageObjects.infraHostsView.clickFlyoutApmServicesLink(); + await pageObjects.assetDetails.clickApmServicesLink(); const url = parse(await browser.getCurrentUrl()); const query = decodeURIComponent(url.query ?? ''); const kuery = 'kuery=host.hostname:"Jennys-MBP.fritz.box"'; @@ -447,52 +374,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); }); - - describe('Host with alerts', () => { - before(async () => { - await pageObjects.timePicker.setAbsoluteRange( - START_DATE.format(timepickerFormat), - END_DATE.format(timepickerFormat) - ); - await pageObjects.infraHostsView.clickHostCheckbox('demo-stack-mysql-01', '-'); - await pageObjects.infraHostsView.clickSelectedHostsButton(); - await pageObjects.infraHostsView.clickSelectedHostsAddFilterButton(); - - await waitForPageToLoad(); - - await pageObjects.infraHostsView.clickTableOpenFlyoutButton(); - }); - - after(async () => { - await retry.try(async () => { - await pageObjects.infraHostsView.clickCloseFlyoutButton(); - }); - }); - - it('should render alerts count for a host inside a flyout', async () => { - await pageObjects.infraHostsView.clickOverviewFlyoutTab(); - - retry.tryForTime(30 * 1000, async () => { - await observability.components.alertSummaryWidget.getFullSizeComponentSelectorOrFail(); - }); - - const activeAlertsCount = - await observability.components.alertSummaryWidget.getActiveAlertCount(); - const totalAlertsCount = - await observability.components.alertSummaryWidget.getTotalAlertCount(); - - expect(activeAlertsCount.trim()).to.equal('2'); - expect(totalAlertsCount.trim()).to.equal('3'); - }); - - it('should render "N/A" when processes summary is not available in flyout', async () => { - await pageObjects.infraHostsView.clickProcessesFlyoutTab(); - const processesTotalValue = - await pageObjects.infraHostsView.getProcessesTabContentTotalValue(); - const processValue = await processesTotalValue.getVisibleText(); - expect(processValue).to.eql('N/A'); - }); - }); }); describe('#Page Content', () => { diff --git a/x-pack/test/functional/apps/infra/index.ts b/x-pack/test/functional/apps/infra/index.ts index e43c9bb47a5dc..b389d56b9032c 100644 --- a/x-pack/test/functional/apps/infra/index.ts +++ b/x-pack/test/functional/apps/infra/index.ts @@ -18,6 +18,7 @@ export default ({ loadTestFile }: FtrProviderContext) => { loadTestFile(require.resolve('./metrics_source_configuration')); loadTestFile(require.resolve('./metrics_anomalies')); loadTestFile(require.resolve('./metrics_explorer')); + loadTestFile(require.resolve('./node_details')); loadTestFile(require.resolve('./hosts_view')); }); diff --git a/x-pack/test/functional/apps/infra/node_details.ts b/x-pack/test/functional/apps/infra/node_details.ts new file mode 100644 index 0000000000000..60b6590eef788 --- /dev/null +++ b/x-pack/test/functional/apps/infra/node_details.ts @@ -0,0 +1,215 @@ +/* + * 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 moment from 'moment'; +import expect from '@kbn/expect'; +import rison from '@kbn/rison'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { DATES, NODE_DETAILS_PATH } from './constants'; + +const START_HOST_ALERTS_DATE = moment.utc(DATES.metricsAndLogs.hosts.min); +const END_HOST_ALERTS_DATE = moment.utc(DATES.metricsAndLogs.hosts.max); +const START_HOST_PROCESSES_DATE = moment.utc(DATES.metricsAndLogs.hosts.processesDataStartDate); +const END_HOST_PROCESSES_DATE = moment.utc(DATES.metricsAndLogs.hosts.processesDataEndDate); + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const observability = getService('observability'); + const browser = getService('browser'); + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + const pageObjects = getPageObjects(['assetDetails', 'common', 'infraHome', 'header']); + + const getNodeDetailsUrl = (assetName: string, dateRange: { from: string; to: string }) => { + const queryParams = new URLSearchParams(); + + queryParams.set( + '_a', + rison.encode({ + autoReload: false, + refreshInterval: 5000, + time: { ...dateRange, interval: '>1m' }, + }) + ); + + queryParams.set('assetName', assetName); + + return queryParams.toString(); + }; + + const navigateToNodeDetails = async ( + assetId: string, + assetName: string, + dateRange: { from: string; to: string } + ) => { + await pageObjects.common.navigateToUrlWithBrowserHistory( + 'infraOps', + `/${NODE_DETAILS_PATH}/${assetId}`, + getNodeDetailsUrl(assetName, dateRange), + { + insertTimestamp: false, + ensureCurrentUrl: false, + useActualUrl: true, + } + ); + }; + + describe('Node Details', () => { + describe('#With Asset Details', () => { + describe('#Asset Type: host', () => { + before(async () => { + await Promise.all([ + esArchiver.load('x-pack/test/functional/es_archives/infra/alerts'), + esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'), + esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_hosts_processes'), + kibanaServer.savedObjects.cleanStandardList(), + ]); + await browser.setWindowSize(1600, 1200); + + await navigateToNodeDetails('Jennys-MBP.fritz.box', 'Jennys-MBP.fritz.box', { + from: START_HOST_PROCESSES_DATE.toISOString(), + to: END_HOST_PROCESSES_DATE.toISOString(), + }); + + await pageObjects.header.waitUntilLoadingHasFinished(); + }); + + after(async () => { + await Promise.all([ + esArchiver.unload('x-pack/test/functional/es_archives/infra/alerts'), + esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs'), + esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_hosts_processes'), + ]); + }); + + describe('Overview Tab', () => { + before(async () => { + await pageObjects.assetDetails.clickOverviewFlyoutTab(); + }); + + [ + { metric: 'cpuUsage', value: '13.9%' }, + { metric: 'normalizedLoad1m', value: '18.8%' }, + { metric: 'memoryUsage', value: '94.9%' }, + { metric: 'diskSpaceUsage', value: 'N/A' }, + ].forEach(({ metric, value }) => { + it(`${metric} tile should show ${value}`, async () => { + await retry.tryForTime(3 * 1000, async () => { + const tileValue = await pageObjects.assetDetails.getAssetDetailsKPITileValue( + metric + ); + expect(tileValue).to.eql(value); + }); + }); + }); + + it('should render 8 charts in the Metrics section', async () => { + const hosts = await pageObjects.assetDetails.getAssetDetailsMetricsCharts(); + expect(hosts.length).to.equal(8); + }); + + it('should show alerts', async () => { + await pageObjects.header.waitUntilLoadingHasFinished(); + await pageObjects.assetDetails.overviewAlertsTitleExists(); + }); + }); + + describe('Metadata Tab', () => { + before(async () => { + await pageObjects.assetDetails.clickMetadataFlyoutTab(); + }); + + it('should show metadata table', async () => { + await pageObjects.assetDetails.metadataTableExists(); + }); + + it('should render metadata tab, pin and unpin table row', async () => { + // Add Pin + await pageObjects.assetDetails.clickAddMetadataPin(); + expect(await pageObjects.assetDetails.metadataRemovePinExists()).to.be(true); + + // Persist pin after refresh + await browser.refresh(); + await retry.try(async () => { + // Temporary until URL state isn't implemented + await pageObjects.assetDetails.clickMetadataFlyoutTab(); + await pageObjects.infraHome.waitForLoading(); + const removePinExist = await pageObjects.assetDetails.metadataRemovePinExists(); + expect(removePinExist).to.be(true); + }); + + // Remove Pin + await pageObjects.assetDetails.clickRemoveMetadataPin(); + expect(await pageObjects.assetDetails.metadataRemovePinExists()).to.be(false); + }); + }); + + describe('Processes Tab', () => { + before(async () => { + await pageObjects.assetDetails.clickProcessesFlyoutTab(); + }); + + it('should render processes tab and with Total Value summary', async () => { + const processesTotalValue = + await pageObjects.assetDetails.getProcessesTabContentTotalValue(); + const processValue = await processesTotalValue.getVisibleText(); + expect(processValue).to.eql('313'); + }); + + it('should expand processes table row', async () => { + await pageObjects.assetDetails.processesTableExists(); + await pageObjects.assetDetails.getProcessesTableBody(); + await pageObjects.assetDetails.clickProcessesTableExpandButton(); + }); + }); + + describe('Logs Tab', () => { + before(async () => { + await pageObjects.assetDetails.clickLogsFlyoutTab(); + }); + it('should render logs tab', async () => { + await testSubjects.existOrFail('infraAssetDetailsLogsTabContent'); + }); + }); + + describe('Host with alerts and no processes', () => { + before(async () => { + await navigateToNodeDetails('demo-stack-mysql-01', 'demo-stack-mysql-01', { + from: START_HOST_ALERTS_DATE.toISOString(), + to: END_HOST_ALERTS_DATE.toISOString(), + }); + }); + + it('should render alerts count for a host inside a flyout', async () => { + await pageObjects.assetDetails.clickOverviewFlyoutTab(); + + retry.tryForTime(30 * 1000, async () => { + await observability.components.alertSummaryWidget.getFullSizeComponentSelectorOrFail(); + }); + + const activeAlertsCount = + await observability.components.alertSummaryWidget.getActiveAlertCount(); + const totalAlertsCount = + await observability.components.alertSummaryWidget.getTotalAlertCount(); + + expect(activeAlertsCount.trim()).to.equal('2'); + expect(totalAlertsCount.trim()).to.equal('3'); + }); + + it('should render "N/A" when processes summary is not available in flyout', async () => { + await pageObjects.assetDetails.clickProcessesFlyoutTab(); + const processesTotalValue = + await pageObjects.assetDetails.getProcessesTabContentTotalValue(); + const processValue = await processesTotalValue.getVisibleText(); + expect(processValue).to.eql('N/A'); + }); + }); + }); + }); + }); +}; diff --git a/x-pack/test/functional/apps/ml/anomaly_detection_result_views/forecasts.ts b/x-pack/test/functional/apps/ml/anomaly_detection_result_views/forecasts.ts index 93ec331230a8a..63f72381d0185 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection_result_views/forecasts.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_result_views/forecasts.ts @@ -42,7 +42,8 @@ export default function ({ getService }: FtrProviderContext) { describe('forecasts', function () { this.tags(['ml']); - describe('with single metric job', function () { + // FLAKY: https://github.com/elastic/kibana/issues/164381 + describe.skip('with single metric job', function () { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); diff --git a/x-pack/test/functional/page_objects/asset_details.ts b/x-pack/test/functional/page_objects/asset_details.ts new file mode 100644 index 0000000000000..ee1084a86987e --- /dev/null +++ b/x-pack/test/functional/page_objects/asset_details.ts @@ -0,0 +1,132 @@ +/* + * 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 { FtrProviderContext } from '../ftr_provider_context'; + +export function AssetDetailsProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + // Overview + async clickOverviewFlyoutTab() { + return testSubjects.click('infraAssetDetailsOverviewTab'); + }, + + async getAssetDetailsKPITileValue(type: string) { + const container = await testSubjects.find('infraAssetDetailsKPIGrid'); + const element = await container.findByTestSubject(`infraAssetDetailsKPI${type}`); + const div = await element.findByClassName('echMetricText__value'); + return div.getAttribute('title'); + }, + + async overviewAlertsTitleExists() { + return testSubjects.existOrFail('infraAssetDetailsAlertsTitle'); + }, + + async getAssetDetailsMetricsCharts() { + const container = await testSubjects.find('infraAssetDetailsMetricsChartGrid'); + return container.findAllByCssSelector('[data-test-subj*="infraAssetDetailsMetricsChart"]'); + }, + + async clickOverviewLinkToAlerts() { + return testSubjects.click('infraAssetDetailsAlertsShowAllButton'); + }, + + async clickOverviewOpenAlertsFlyout() { + return testSubjects.click('infraAssetDetailsCreateAlertsRuleButton'); + }, + + async clickShowAllMetadataOverviewTab() { + return testSubjects.click('infraAssetDetailsMetadataShowAllButton'); + }, + + async clickApmServicesLink() { + return testSubjects.click('infraAssetDetailsViewAPMServicesButton'); + }, + + // Metadata + async clickMetadataFlyoutTab() { + return testSubjects.click('infraAssetDetailsMetadataTab'); + }, + + async clickAddMetadataPin() { + return testSubjects.click('infraAssetDetailsMetadataAddPin'); + }, + + async clickRemoveMetadataPin() { + return testSubjects.click('infraAssetDetailsMetadataRemovePin'); + }, + + async clickAddMetadataFilter() { + return testSubjects.click('infraAssetDetailsMetadataAddFilterButton'); + }, + + async clickRemoveMetadataFilter() { + return testSubjects.click('infraAssetDetailsMetadataRemoveFilterButton'); + }, + + async metadataTableExists() { + return testSubjects.existOrFail('infraAssetDetailsMetadataTable'); + }, + + async metadataRemovePinExists() { + return testSubjects.exists('infraAssetDetailsMetadataRemovePin'); + }, + + async getMetadataAppliedFilter() { + const filter = await testSubjects.find( + "filter-badge-'host.architecture: arm64' filter filter-enabled filter-key-host.architecture filter-value-arm64 filter-unpinned filter-id-0" + ); + return filter.getVisibleText(); + }, + + async metadataRemoveFilterExists() { + return testSubjects.exists('infraAssetDetailsMetadataRemoveFilterButton'); + }, + + // Processes + async clickProcessesFlyoutTab() { + return testSubjects.click('infraAssetDetailsProcessesTab'); + }, + + async getProcessesTabContentTitle(index: number) { + const processesListElements = await testSubjects.findAll( + 'infraAssetDetailsProcessesSummaryTableItem' + ); + return processesListElements[index].findByCssSelector('dt'); + }, + + async getProcessesTabContentTotalValue() { + const processesListElements = await testSubjects.findAll( + 'infraAssetDetailsProcessesSummaryTableItem' + ); + return processesListElements[0].findByCssSelector('dd'); + }, + + async processesTableExists() { + return testSubjects.existOrFail('infraAssetDetailsProcessesTable'); + }, + + async getProcessesTableBody() { + const processesTable = await testSubjects.find('infraAssetDetailsProcessesTable'); + return processesTable.findByCssSelector('tbody'); + }, + + async clickProcessesTableExpandButton() { + return testSubjects.click('infraProcessRowButton'); + }, + + // Logs + async clickLogsFlyoutTab() { + return testSubjects.click('infraAssetDetailsLogsTab'); + }, + + async logsExists() { + await testSubjects.existOrFail('infraAssetDetailsLogsTabContent'); + }, + }; +} diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 786b8294a1347..7e0409b3c19d1 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -9,6 +9,7 @@ import { pageObjects as kibanaFunctionalPageObjects } from '../../../../test/fun import { AccountSettingsPageObject } from './account_settings_page'; import { ApiKeysPageProvider } from './api_keys_page'; +import { AssetDetailsProvider } from './asset_details'; import { BannersPageObject } from './banners_page'; import { CanvasPageProvider } from './canvas_page'; import { CopySavedObjectsToSpacePageProvider } from './copy_saved_objects_to_space_page'; @@ -54,6 +55,7 @@ export const pageObjects = { ...kibanaFunctionalPageObjects, accountSetting: AccountSettingsPageObject, apiKeys: ApiKeysPageProvider, + assetDetails: AssetDetailsProvider, banners: BannersPageObject, canvas: CanvasPageProvider, copySavedObjectsToSpace: CopySavedObjectsToSpacePageProvider, diff --git a/x-pack/test/functional/page_objects/infra_hosts_view.ts b/x-pack/test/functional/page_objects/infra_hosts_view.ts index 25e4b0302a763..e449fb8f05b57 100644 --- a/x-pack/test/functional/page_objects/infra_hosts_view.ts +++ b/x-pack/test/functional/page_objects/infra_hosts_view.ts @@ -45,60 +45,6 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { return await testSubjects.click('inventory-hostsView-link-badge'); }, - // Asset Details Flyout - - async clickOverviewFlyoutTab() { - return testSubjects.click('infraAssetDetailsOverviewTab'); - }, - - async clickMetadataFlyoutTab() { - return testSubjects.click('infraAssetDetailsMetadataTab'); - }, - - async clickProcessesFlyoutTab() { - return testSubjects.click('infraAssetDetailsProcessesTab'); - }, - - async clickLogsFlyoutTab() { - return testSubjects.click('infraAssetDetailsLogsTab'); - }, - - async clickOverviewLinkToAlerts() { - return testSubjects.click('infraAssetDetailsAlertsShowAllButton'); - }, - - async clickOverviewOpenAlertsFlyout() { - return testSubjects.click('infraAssetDetailsCreateAlertsRuleButton'); - }, - - async clickShowAllMetadataOverviewTab() { - return testSubjects.click('infraAssetDetailsMetadataShowAllButton'); - }, - - async clickProcessesTableExpandButton() { - return testSubjects.click('infraProcessRowButton'); - }, - - async clickFlyoutApmServicesLink() { - return testSubjects.click('infraAssetDetailsViewAPMServicesButton'); - }, - - async clickAddMetadataPin() { - return testSubjects.click('infraAssetDetailsMetadataAddPin'); - }, - - async clickRemoveMetadataPin() { - return testSubjects.click('infraAssetDetailsMetadataRemovePin'); - }, - - async clickAddMetadataFilter() { - return testSubjects.click('infraAssetDetailsMetadataAddFilterButton'); - }, - - async clickRemoveMetadataFilter() { - return testSubjects.click('infraAssetDetailsMetadataRemoveFilterButton'); - }, - // Splash screen async getHostsLandingPageDisabled() { @@ -208,65 +154,6 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { return div.getAttribute('title'); }, - // Asset Details Flyout Tabs - async getAssetDetailsKPITileValue(type: string) { - const container = await testSubjects.find('infraAssetDetailsKPIGrid'); - const element = await container.findByTestSubject(`infraAssetDetailsKPI${type}`); - const div = await element.findByClassName('echMetricText__value'); - return div.getAttribute('title'); - }, - - overviewAlertsTitleExist() { - return testSubjects.exists('infraAssetDetailsAlertsTitle'); - }, - - async getAssetDetailsMetricsCharts() { - const container = await testSubjects.find('infraAssetDetailsMetricsChartGrid'); - return container.findAllByCssSelector('[data-test-subj*="infraAssetDetailsMetricsChart"]'); - }, - - metadataTableExist() { - return testSubjects.exists('infraAssetDetailsMetadataTable'); - }, - - async getRemovePinExist() { - return testSubjects.exists('infraAssetDetailsMetadataRemovePin'); - }, - - async getAppliedFilter() { - const filter = await testSubjects.find( - "filter-badge-'host.architecture: arm64' filter filter-enabled filter-key-host.architecture filter-value-arm64 filter-unpinned filter-id-0" - ); - return filter.getVisibleText(); - }, - - async getRemoveFilterExist() { - return testSubjects.exists('infraAssetDetailsMetadataRemoveFilterButton'); - }, - - async getProcessesTabContentTitle(index: number) { - const processesListElements = await testSubjects.findAll( - 'infraAssetDetailsProcessesSummaryTableItem' - ); - return processesListElements[index].findByCssSelector('dt'); - }, - - async getProcessesTabContentTotalValue() { - const processesListElements = await testSubjects.findAll( - 'infraAssetDetailsProcessesSummaryTableItem' - ); - return processesListElements[0].findByCssSelector('dd'); - }, - - getProcessesTable() { - return testSubjects.find('infraAssetDetailsProcessesTable'); - }, - - async getProcessesTableBody() { - const processesTable = await this.getProcessesTable(); - return processesTable.findByCssSelector('tbody'); - }, - // Logs Tab getLogsTab() { return testSubjects.find('hostsView-tabs-logs'); diff --git a/x-pack/test/reporting_functional/reporting_and_timeout.config.ts b/x-pack/test/reporting_functional/reporting_and_timeout.config.ts deleted file mode 100644 index 05e77ea70cd6f..0000000000000 --- a/x-pack/test/reporting_functional/reporting_and_timeout.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrConfigProviderContext } from '@kbn/test'; -import { resolve } from 'path'; - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('./reporting_and_security.config')); - - return { - ...functionalConfig.getAll(), - junit: { reportName: 'X-Pack Reporting Functional Tests: Reports and Timeout Handling' }, - testFiles: [resolve(__dirname, './reporting_and_timeout')], - kbnTestServer: { - ...functionalConfig.get('kbnTestServer'), - serverArgs: [ - ...functionalConfig.get('kbnTestServer.serverArgs'), - `--xpack.reporting.capture.timeouts.waitForElements=1s`, - ], - }, - }; -} diff --git a/x-pack/test/reporting_functional/reporting_and_timeout/fixtures/baseline/warnings_capture_b.png b/x-pack/test/reporting_functional/reporting_and_timeout/fixtures/baseline/warnings_capture_b.png deleted file mode 100644 index e86939f7ff741..0000000000000 Binary files a/x-pack/test/reporting_functional/reporting_and_timeout/fixtures/baseline/warnings_capture_b.png and /dev/null differ diff --git a/x-pack/test/reporting_functional/reporting_and_timeout/index.ts b/x-pack/test/reporting_functional/reporting_and_timeout/index.ts deleted file mode 100644 index c420c76d77304..0000000000000 --- a/x-pack/test/reporting_functional/reporting_and_timeout/index.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import fs from 'fs'; -import path from 'path'; -import { PNG } from 'pngjs'; -import { FtrProviderContext } from '../ftr_provider_context'; - -// eslint-disable-next-line import/no-default-export -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['common', 'reporting', 'dashboard']); - const log = getService('log'); - const reportingAPI = getService('reportingAPI'); - const reportingFunctional = getService('reportingFunctional'); - const browser = getService('browser'); - const png = getService('png'); - const config = getService('config'); - const screenshotDir = config.get('screenshots.directory'); - - // FLAKY: https://github.com/elastic/kibana/issues/135309 - describe.skip('Reporting Functional Tests with forced timeout', function () { - const dashboardTitle = 'Ecom Dashboard Hidden Panel Titles'; - const sessionPngFullPage = 'warnings_capture_session_a'; - const sessionPngCropped = 'warnings_capture_session_b'; - const baselinePng = path.resolve(__dirname, 'fixtures/baseline/warnings_capture_b.png'); - - let url: string; - before(async () => { - await reportingAPI.logTaskManagerHealth(); - await reportingFunctional.initEcommerce(); - - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.loadSavedDashboard(dashboardTitle); - await PageObjects.reporting.setTimepickerInEcommerceDataRange(); - await browser.setWindowSize(800, 850); - - await PageObjects.reporting.openPngReportingPanel(); - await PageObjects.reporting.clickGenerateReportButton(); - - url = await PageObjects.reporting.getReportURL(60000); - - const res = await PageObjects.reporting.getResponse(url); - expect(res.status).to.equal(200); - expect(res.get('content-type')).to.equal('image/png'); - }); - - after(async () => { - await reportingFunctional.teardownEcommerce(); - }); - - it('adds a visual warning in the report output', async () => { - const captureData = await PageObjects.reporting.getRawPdfReportData(url); - const sessionReport = await PageObjects.reporting.writeSessionReport( - sessionPngFullPage, - 'png', - captureData, - screenshotDir - ); - - const region = { height: 320, width: 1540, srcX: 20, srcY: 10 }; - const dstPath = path.resolve(screenshotDir, sessionPngCropped + '.png'); - const dst = new PNG({ width: region.width, height: region.height }); - - const pngSessionFilePath = await new Promise((resolve) => { - fs.createReadStream(sessionReport) - .pipe(new PNG()) - .on('parsed', function () { - log.info(`cropping report to the visual warning area`); - this.bitblt(dst, region.srcX, region.srcY, region.width, region.height, 0, 0); - dst.pack().pipe(fs.createWriteStream(dstPath)); - resolve(dstPath); - }); - }); - - log.info(`saved cropped file to ${dstPath}`); - - expect( - await png.checkIfPngsMatch(pngSessionFilePath, baselinePng, screenshotDir) - ).to.be.lessThan(0.09); - - /** - * This test may fail when styling differences affect the result. To update the snapshot: - * - * 1. Run the functional test, to generate new temporary files for screenshot comparison. - * 2. Save the screenshot as the new baseline file: - * cp \ - * x-pack/test/functional/screenshots/session/warnings_capture_session_b_actual.png \ - * x-pack/test/reporting_functional/reporting_and_timeout/fixtures/baseline/warnings_capture_b.png - * 3. Commit the changes to the .png file - */ - }); - }); -} diff --git a/x-pack/test/saved_object_tagging/functional/tests/bulk_actions.ts b/x-pack/test/saved_object_tagging/functional/tests/bulk_actions.ts index f0f2d3aa980ac..d4aef2d44856f 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/bulk_actions.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/bulk_actions.ts @@ -29,6 +29,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('bulk delete', () => { it('deletes multiple tags', async () => { + const initialDisplayedTags = await tagManagementPage.getDisplayedTagNames(); await tagManagementPage.selectTagByName('tag-1'); await tagManagementPage.selectTagByName('tag-3'); @@ -38,7 +39,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await tagManagementPage.waitUntilTableIsLoaded(); const displayedTags = await tagManagementPage.getDisplayedTagNames(); - expect(displayedTags.length).to.be(3); + expect(displayedTags.length).to.be(initialDisplayedTags.length - 2); expect(displayedTags).to.eql(['my-favorite-tag', 'tag with whitespace', 'tag-2']); }); }); diff --git a/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_connectors/connector_types.ts b/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_connectors/connector_types.ts index 7b4b35d64f389..f4907a1df0969 100644 --- a/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_connectors/connector_types.ts +++ b/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_connectors/connector_types.ts @@ -23,6 +23,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { `"rule_name": "{{rule.name}}",\n` + `"alert_id": "{{alert.id}}",\n` + `"context_message": "{{context.message}}"\n`; + const webhookJson = + `{\n` + + `"short_description": "{{context.rule.name}}",\n` + + `"description": "{{context.rule.description}}"`; const emailConnectorName = 'my-email-connector'; describe('connector types', function () { @@ -151,5 +155,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 1024 ); }); + + it('webhook connector screenshots', async () => { + await pageObjects.common.navigateToApp('connectors'); + await pageObjects.header.waitUntilLoadingHasFinished(); + await actions.common.openNewConnectorForm('webhook'); + await testSubjects.setValue('nameInput', 'Webhook test connector'); + await testSubjects.setValue('webhookUrlText', 'https://example.com'); + await testSubjects.setValue('webhookUserInput', 'testuser'); + await testSubjects.setValue('webhookPasswordInput', 'password'); + await commonScreenshots.takeScreenshot('webhook-connector', screenshotDirectories); + const saveTestButton = await testSubjects.find('create-connector-flyout-save-test-btn'); + await saveTestButton.click(); + await testSubjects.setValue('actionJsonEditor', webhookJson); + await commonScreenshots.takeScreenshot('webhook-params-test', screenshotDirectories); + await testSubjects.click('euiFlyoutCloseButton'); + }); }); } diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/url_state.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/url_state.cy.ts new file mode 100644 index 0000000000000..8eed754a18600 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/url_state.cy.ts @@ -0,0 +1,69 @@ +/* + * 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 { encode } from '@kbn/rison'; +import { tag } from '../../../tags'; + +import { getTimeline } from '../../../objects/timeline'; + +import { TIMELINE_HEADER } from '../../../screens/timeline'; + +import { createTimeline } from '../../../tasks/api_calls/timelines'; + +import { cleanKibana } from '../../../tasks/common'; +import { ALERTS_URL } from '../../../urls/navigation'; +import { createRule } from '../../../tasks/api_calls/rules'; +import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; +import { getNewRule } from '../../../objects/rule'; + +import { login, visitWithoutDateRange, visit } from '../../../tasks/login'; + +import { TIMELINES_URL } from '../../../urls/navigation'; + +describe('Open timeline', { tags: [tag.BROKEN_IN_SERVERLESS, tag.ESS] }, () => { + let timelineSavedObjectId: string | null = null; + before(function () { + cleanKibana(); + login(); + visitWithoutDateRange(TIMELINES_URL); + + createTimeline(getTimeline()).then((response) => { + timelineSavedObjectId = response.body.data.persistTimeline.timeline.savedObjectId; + return response.body.data.persistTimeline.timeline.savedObjectId; + }); + + createRule(getNewRule()); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + }); + + beforeEach(() => { + login(); + }); + + describe('open timeline from url exclusively', () => { + it('should open a timeline via url alone without a saved object id', () => { + const urlWithoutSavedObjectId = `${ALERTS_URL}?timeline=(activeTab:query,isOpen:!t)`; + visitWithoutDateRange(urlWithoutSavedObjectId); + cy.get(TIMELINE_HEADER).should('be.visible'); + }); + + it('should also support opening with a saved object id', () => { + cy.location('search').then((search) => { + const params = new URLSearchParams(search); + const timelineParams = encode({ + activeTab: 'query', + isOpen: true, + id: timelineSavedObjectId, + }); + params.set('timeline', timelineParams); + const urlWithSavedObjectId = `${ALERTS_URL}?${params.toString()}`; + visitWithoutDateRange(urlWithSavedObjectId); + cy.get(TIMELINE_HEADER).should('be.visible'); + }); + }); + }); +}); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/urls/state.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/urls/state.cy.ts index b696f4943ad22..8b6a6c2587be4 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/urls/state.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/urls/state.cy.ts @@ -225,11 +225,12 @@ describe('url state', { tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] }, () => { navigateFromHeaderTo(HOSTS); openNavigationPanel(EXPLORE_PANEL_BTN); - cy.get(NETWORK).should( - 'have.attr', - 'href', - `/app/security/network?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))` - ); + cy.get(NETWORK) + .should('have.attr', 'href') + .and( + 'contain', + `/app/security/network?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))` + ); }); it('sets KQL in host page and detail page and check if href match on breadcrumb, tabs and subTabs', () => { @@ -239,40 +240,43 @@ describe('url state', { tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] }, () => { waitForAllHostsToBeLoaded(); openNavigationPanel(EXPLORE_PANEL_BTN); - cy.get(HOSTS).should( - 'have.attr', - 'href', - `/app/security/hosts?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))` - ); - cy.get(NETWORK).should( - 'have.attr', - 'href', - `/app/security/network?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))` - ); + cy.get(HOSTS) + .should('have.attr', 'href') + .and( + 'contain', + `/app/security/hosts?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))` + ); + cy.get(NETWORK) + .should('have.attr', 'href') + .and( + 'contain', + `/app/security/network?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))` + ); cy.get(HOSTS_NAMES).first().should('have.text', 'siem-kibana'); openFirstHostDetails(); clearSearchBar(); kqlSearch('agent.type: "auditbeat" {enter}'); - cy.get(ANOMALIES_TAB).should( - 'have.attr', - 'href', - "/app/security/hosts/name/siem-kibana/anomalies?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))&query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')" - ); + cy.get(ANOMALIES_TAB) + .should('have.attr', 'href') + .and( + 'contain', + "/app/security/hosts/name/siem-kibana/anomalies?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))" + ); cy.get(BREADCRUMBS) .eq(2) - .should( - 'have.attr', - 'href', + .should('have.attr', 'href') + .and( + 'contain', `/app/security/hosts?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))` ); cy.get(BREADCRUMBS) .eq(3) - .should( - 'have.attr', - 'href', + .should('have.attr', 'href') + .and( + 'contain', `/app/security/hosts/name/siem-kibana?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))` ); }); diff --git a/x-pack/test_serverless/README.md b/x-pack/test_serverless/README.md index 9223dee92a2b9..84b1aaab676d6 100644 --- a/x-pack/test_serverless/README.md +++ b/x-pack/test_serverless/README.md @@ -97,6 +97,33 @@ tests that should run in a serverless environment have to be added to the Tests in this area should be clearly designed for the serverless environment, particularly when it comes to timing for API requests and UI interaction. +### Testing with feature flags + +**tl;dr:** Tests specific to functionality behind a feature flag need special +handling and are by default only tested locally / in CI but excluded from regular +test runs in MKI. + +New features might be gated behind a feature flag and can only be enabled +through a yml configuration entry. By default, these features are not enabled +so they're not available in a regular serverless MKI project, which would make +end-to-end tests for such a feature fail. In order to still have tests for +features behind a feature flag, these tests need to be separated from the +regular tests. + +For every project's `test_suites` directory, there are feature flags specific +config (`config.feature_flags.ts`) and index (`index.feature_flags.ts`) files +next to the regular `config.ts` and `index.ts`. These extra files are used to +cover all feature flag tests of the respective area. +If you want to add feature flag specific tests: +- Add your feature flag(s) to the `kbnServerArgs` in the `config.feature_flags.ts` file +- Load your test file(s) in the `index.feature_flags.ts` file + +As mentioned above, these tests are not part of the regular test run against MKI +projects. If you still want to run feature flag tests against an MKI project, +this requires a Kibana docker build that has the feature flags enabled by default. +This docker image can then be used to create a project in serverless QA and the +feature flags tests can be pointed to the project. + ## Run tests Similar to how functional tests are run in `x-pack/test`, you can point the functional tests server and test runner to config files in this `x-pack/test_serverless` diff --git a/x-pack/test_serverless/api_integration/config.base.ts b/x-pack/test_serverless/api_integration/config.base.ts index 447950aff50cc..4ffdbfeef108a 100644 --- a/x-pack/test_serverless/api_integration/config.base.ts +++ b/x-pack/test_serverless/api_integration/config.base.ts @@ -25,9 +25,7 @@ export function createTestConfig(options: CreateTestConfigOptions) { serverArgs: [ ...svlSharedConfig.get('kbnTestServer.serverArgs'), `--serverless=${options.serverlessProject}`, - `--xpack.alerting.enableFrameworkAlerts=true`, - '--xpack.observability.unsafe.thresholdRule.enabled=true', - '--server.publicBaseUrl=https://localhost:5601', + ...(options.kbnServerArgs || []), ], }, testFiles: options.testFiles, diff --git a/x-pack/test_serverless/api_integration/services/alerting_api.ts b/x-pack/test_serverless/api_integration/services/alerting_api.ts index 33d8b224561cc..8eb771d7eb11d 100644 --- a/x-pack/test_serverless/api_integration/services/alerting_api.ts +++ b/x-pack/test_serverless/api_integration/services/alerting_api.ts @@ -10,6 +10,9 @@ import type { SearchResponse, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { MetricThresholdParams } from '@kbn/infra-plugin/common/alerting/metrics'; +import { ThresholdParams } from '@kbn/observability-plugin/common/threshold_rule/types'; + import { FtrProviderContext } from '../ftr_provider_context'; export function AlertingApiProvider({ getService }: FtrProviderContext) { @@ -86,5 +89,56 @@ export function AlertingApiProvider({ getService }: FtrProviderContext) { return response; }); }, + + async createIndexConnector({ name, indexName }: { name: string; indexName: string }) { + const { body } = await supertest + .post(`/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .set('x-elastic-internal-origin', 'foo') + .send({ + name, + config: { + index: indexName, + refresh: true, + }, + connector_type_id: '.index', + }); + return body.id as string; + }, + + async createRule({ + name, + ruleTypeId, + params, + actions = [], + tags = [], + schedule, + consumer, + }: { + ruleTypeId: string; + name: string; + params: MetricThresholdParams | ThresholdParams; + actions?: any[]; + tags?: any[]; + schedule?: { interval: string }; + consumer: string; + }) { + const { body } = await supertest + .post(`/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .set('x-elastic-internal-origin', 'foo') + .send({ + params, + consumer, + schedule: schedule || { + interval: '5m', + }, + tags, + name, + rule_type_id: ruleTypeId, + actions, + }); + return body; + }, }; } diff --git a/x-pack/test_serverless/api_integration/services/data_view_api.ts b/x-pack/test_serverless/api_integration/services/data_view_api.ts new file mode 100644 index 0000000000000..430ff60aac1e2 --- /dev/null +++ b/x-pack/test_serverless/api_integration/services/data_view_api.ts @@ -0,0 +1,52 @@ +/* + * 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 { FtrProviderContext } from '../ftr_provider_context'; + +export function DataViewApiProvider({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + return { + async create({ id, name, title }: { id: string; name: string; title: string }) { + const { body } = await supertest + .post(`/api/content_management/rpc/create`) + .set('kbn-xsrf', 'foo') + .set('x-elastic-internal-origin', 'foo') + .send({ + contentTypeId: 'index-pattern', + data: { + fieldAttrs: '{}', + title, + timeFieldName: '@timestamp', + sourceFilters: '[]', + fields: '[]', + fieldFormatMap: '{}', + typeMeta: '{}', + runtimeFieldMap: '{}', + name, + }, + options: { id }, + version: 1, + }); + return body; + }, + + async delete({ id }: { id: string }) { + const { body } = await supertest + .post(`/api/content_management/rpc/delete`) + .set('kbn-xsrf', 'foo') + .set('x-elastic-internal-origin', 'foo') + .send({ + contentTypeId: 'index-pattern', + id, + options: { force: true }, + version: 1, + }); + return body; + }, + }; +} diff --git a/x-pack/test_serverless/api_integration/services/index.ts b/x-pack/test_serverless/api_integration/services/index.ts index 89a466de1b3f1..06e7c33fd7099 100644 --- a/x-pack/test_serverless/api_integration/services/index.ts +++ b/x-pack/test_serverless/api_integration/services/index.ts @@ -13,6 +13,7 @@ import { services as svlSharedServices } from '../../shared/services'; import { SvlCommonApiServiceProvider } from './svl_common_api'; import { AlertingApiProvider } from './alerting_api'; import { SamlToolsProvider } from './saml_tools'; +import { DataViewApiProvider } from './data_view_api'; export const services = { ...xpackApiIntegrationServices, @@ -21,6 +22,7 @@ export const services = { svlCommonApi: SvlCommonApiServiceProvider, alertingApi: AlertingApiProvider, samlTools: SamlToolsProvider, + dataViewApi: DataViewApiProvider, }; export type InheritedFtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/index.ts index b8fc54146364f..254deb3ce4270 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/index.ts @@ -25,5 +25,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./rollups')); loadTestFile(require.resolve('./index_management')); loadTestFile(require.resolve('./alerting')); + loadTestFile(require.resolve('./ingest_pipelines')); }); } diff --git a/x-pack/test_serverless/api_integration/test_suites/common/ingest_pipelines.ts b/x-pack/test_serverless/api_integration/test_suites/common/ingest_pipelines.ts new file mode 100644 index 0000000000000..1e5ee6d39bb71 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/ingest_pipelines.ts @@ -0,0 +1,440 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { IngestPutPipelineRequest } from '@elastic/elasticsearch/lib/api/types'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const ingestPipelines = getService('ingestPipelines'); + const log = getService('log'); + + describe('Ingest Pipelines', function () { + after(async () => { + await ingestPipelines.api.deletePipelines(); + }); + + describe('Create', () => { + it('should create a pipeline', async () => { + const pipelineRequestBody = ingestPipelines.fixtures.createPipelineBody(); + const { body } = await supertest + .post(ingestPipelines.fixtures.apiBasePath) + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'xxx') + .send(pipelineRequestBody); + + expect(body).to.eql({ + acknowledged: true, + }); + }); + + it('should create a pipeline with only required fields', async () => { + const pipelineRequestBody = ingestPipelines.fixtures.createPipelineBodyWithRequiredFields(); // Includes name and processors[] only + + const { body } = await supertest + .post(ingestPipelines.fixtures.apiBasePath) + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'xxx') + .send(pipelineRequestBody) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + }); + + it('should not allow creation of an existing pipeline', async () => { + const pipelineRequestBody = ingestPipelines.fixtures.createPipelineBodyWithRequiredFields(); // Includes name and processors[] only + const { name, ...esPipelineRequestBody } = pipelineRequestBody; + + // First, create a pipeline using the ES API + await ingestPipelines.api.createPipeline({ id: name, ...esPipelineRequestBody }); + + // Then, create a pipeline with our internal API + const { body } = await supertest + .post(ingestPipelines.fixtures.apiBasePath) + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'xxx') + .send(pipelineRequestBody) + .expect(409); + + expect(body).to.eql({ + statusCode: 409, + error: 'Conflict', + message: `There is already a pipeline with name '${name}'.`, + }); + }); + }); + + describe('Update', () => { + let pipeline: Omit; + let pipelineName: string; + + before(async () => { + // Create pipeline that can be used to test PUT request + try { + const pipelineRequestBody = + ingestPipelines.fixtures.createPipelineBodyWithRequiredFields(); + const { name, ...esPipelineRequestBody } = pipelineRequestBody; + + pipeline = esPipelineRequestBody; + pipelineName = name; + await ingestPipelines.api.createPipeline({ id: name, ...esPipelineRequestBody }); + } catch (err) { + log.debug('[Setup error] Error creating ingest pipeline'); + throw err; + } + }); + + it('should allow an existing pipeline to be updated', async () => { + const uri = `${ingestPipelines.fixtures.apiBasePath}/${pipelineName}`; + + const { body } = await supertest + .put(uri) + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'xxx') + .send({ + ...pipeline, + description: 'updated test pipeline description', + _meta: { + field_1: 'updated', + new_field: 3, + }, + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + }); + + it('should allow optional fields to be removed', async () => { + const uri = `${ingestPipelines.fixtures.apiBasePath}/${pipelineName}`; + + const { body } = await supertest + .put(uri) + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'xxx') + .send({ + // removes description, version, on_failure, and _meta + processors: pipeline.processors, + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + }); + + it('should not allow a non-existing pipeline to be updated', async () => { + const uri = `${ingestPipelines.fixtures.apiBasePath}/pipeline_does_not_exist`; + + const { body } = await supertest + .put(uri) + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'xxx') + .send({ + ...pipeline, + description: 'updated test pipeline description', + _meta: { + field_1: 'updated', + new_field: 3, + }, + }) + .expect(404); + + expect(body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: '{}', + attributes: {}, + }); + }); + }); + + describe('Get', () => { + let pipeline: Omit; + let pipelineName: string; + + before(async () => { + // Create pipeline that can be used to test GET request + try { + const pipelineRequestBody = + ingestPipelines.fixtures.createPipelineBodyWithRequiredFields(); + const { name, ...esPipelineRequestBody } = pipelineRequestBody; + + pipeline = esPipelineRequestBody; + pipelineName = name; + await ingestPipelines.api.createPipeline({ id: name, ...esPipelineRequestBody }); + } catch (err) { + log.debug('[Setup error] Error creating ingest pipeline'); + throw err; + } + }); + + describe('all pipelines', () => { + it('should return an array of pipelines', async () => { + const { body } = await supertest + .get(ingestPipelines.fixtures.apiBasePath) + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'xxx') + .expect(200); + + expect(Array.isArray(body)).to.be(true); + + // There are some pipelines created OOTB with ES + // To not be dependent on these, we only confirm the pipeline we created as part of the test exists + const testPipeline = body.find(({ name }: { name: string }) => name === pipelineName); + + expect(testPipeline).to.eql({ + ...pipeline, + isManaged: false, + name: pipelineName, + }); + }); + }); + describe('one pipeline', () => { + it('should return a single pipeline', async () => { + const uri = `${ingestPipelines.fixtures.apiBasePath}/${pipelineName}`; + + const { body } = await supertest + .get(uri) + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'xxx') + .expect(200); + + expect(body).to.eql({ + ...pipeline, + isManaged: false, + name: pipelineName, + }); + }); + }); + }); + + describe('Delete', () => { + const pipelineIds: string[] = []; + + before(async () => { + const pipelineA = ingestPipelines.fixtures.createPipelineBodyWithRequiredFields(); + const pipelineB = ingestPipelines.fixtures.createPipelineBodyWithRequiredFields(); + const pipelineC = ingestPipelines.fixtures.createPipelineBodyWithRequiredFields(); + const pipelineD = ingestPipelines.fixtures.createPipelineBodyWithRequiredFields(); + + // Create several pipelines that can be used to test deletion + await Promise.all( + [pipelineA, pipelineB, pipelineC, pipelineD].map((pipeline) => { + const { name, ...pipelineRequestBody } = pipeline; + pipelineIds.push(pipeline.name); + return ingestPipelines.api.createPipeline({ id: name, ...pipelineRequestBody }); + }) + ).catch((err) => { + log.debug(`[Setup error] Error creating pipelines: ${err.message}`); + throw err; + }); + }); + + it('should delete a pipeline', async () => { + const pipelineA = pipelineIds[0]; + + const uri = `${ingestPipelines.fixtures.apiBasePath}/${pipelineA}`; + + const { body } = await supertest + .delete(uri) + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'xxx') + .expect(200); + + expect(body).to.eql({ + itemsDeleted: [pipelineA], + errors: [], + }); + }); + + it('should delete multiple pipelines', async () => { + const pipelineB = pipelineIds[1]; + const pipelineC = pipelineIds[2]; + const uri = `${ingestPipelines.fixtures.apiBasePath}/${pipelineB},${pipelineC}`; + + const { + body: { itemsDeleted, errors }, + } = await supertest + .delete(uri) + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'xxx') + .expect(200); + + expect(errors).to.eql([]); + + // The itemsDeleted array order isn't guaranteed, so we assert against each pipeline name instead + [pipelineB, pipelineC].forEach((pipelineName) => { + expect(itemsDeleted.includes(pipelineName)).to.be(true); + }); + }); + + it('should return an error for any pipelines not sucessfully deleted', async () => { + const PIPELINE_DOES_NOT_EXIST = 'pipeline_does_not_exist'; + const pipelineD = pipelineIds[3]; + + const uri = `${ingestPipelines.fixtures.apiBasePath}/${pipelineD},${PIPELINE_DOES_NOT_EXIST}`; + + const { body } = await supertest + .delete(uri) + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'xxx') + .expect(200); + + expect(body).to.eql({ + itemsDeleted: [pipelineD], + errors: [ + { + name: PIPELINE_DOES_NOT_EXIST, + error: { + root_cause: [ + { + type: 'resource_not_found_exception', + reason: 'pipeline [pipeline_does_not_exist] is missing', + }, + ], + type: 'resource_not_found_exception', + reason: 'pipeline [pipeline_does_not_exist] is missing', + }, + status: 404, + }, + ], + }); + }); + }); + + describe('Simulate', () => { + it('should successfully simulate a pipeline', async () => { + const { name, ...pipeline } = ingestPipelines.fixtures.createPipelineBody(); + const documents = ingestPipelines.fixtures.createDocuments(); + const { body } = await supertest + .post(`${ingestPipelines.fixtures.apiBasePath}/simulate`) + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'xxx') + .send({ + pipeline, + documents, + }) + .expect(200); + + // The simulate ES response is quite long and includes timestamps + // so for now, we just confirm the docs array is returned with the correct length + expect(body.docs?.length).to.eql(2); + }); + + it('should successfully simulate a pipeline with only required pipeline fields', async () => { + const { name, ...pipeline } = + ingestPipelines.fixtures.createPipelineBodyWithRequiredFields(); + const documents = ingestPipelines.fixtures.createDocuments(); + const { body } = await supertest + .post(`${ingestPipelines.fixtures.apiBasePath}/simulate`) + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'xxx') + .send({ + pipeline, + documents, + }) + .expect(200); + + // The simulate ES response is quite long and includes timestamps + // so for now, we just confirm the docs array is returned with the correct length + expect(body.docs?.length).to.eql(2); + }); + }); + + describe('Fetch documents', () => { + const INDEX = 'test_index'; + const DOCUMENT_ID = '1'; + const DOCUMENT = { + name: 'John Doe', + }; + + before(async () => { + // Create an index with a document that can be used to test GET request + try { + await ingestPipelines.api.createIndex({ id: DOCUMENT_ID, index: INDEX, body: DOCUMENT }); + } catch (err) { + log.debug('[Setup error] Error creating index'); + throw err; + } + }); + + after(async () => { + // Clean up index created + try { + await ingestPipelines.api.deleteIndex(INDEX); + } catch (err) { + log.debug('[Cleanup error] Error deleting index'); + throw err; + } + }); + + it('should return a document', async () => { + const uri = `${ingestPipelines.fixtures.apiBasePath}/documents/${INDEX}/${DOCUMENT_ID}`; + + const { body } = await supertest + .get(uri) + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'xxx') + .expect(200); + + expect(body).to.eql({ + _index: INDEX, + _id: DOCUMENT_ID, + _source: DOCUMENT, + }); + }); + + it('should return an error if the document does not exist', async () => { + const uri = `${ingestPipelines.fixtures.apiBasePath}/documents/${INDEX}/2`; // Document 2 does not exist + + const { body } = await supertest + .get(uri) + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'xxx') + .expect(404); + + expect(body).to.eql({ + error: 'Not Found', + message: '{"_index":"test_index","_id":"2","found":false}', + statusCode: 404, + attributes: {}, + }); + }); + }); + + describe('Map CSV to pipeline', () => { + it('should map to a pipeline', async () => { + const validCsv = + 'source_field,copy_action,format_action,timestamp_format,destination_field,Notes\nsrcip,,,,source.address,Copying srcip to source.address'; + const { body } = await supertest + .post(`${ingestPipelines.fixtures.apiBasePath}/parse_csv`) + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'xxx') + .send({ + copyAction: 'copy', + file: validCsv, + }) + .expect(200); + + expect(body.processors).to.eql([ + { + set: { + field: 'source.address', + value: '{{srcip}}', + if: 'ctx.srcip != null', + }, + }, + ]); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/cases/index.ts b/x-pack/test_serverless/api_integration/test_suites/observability/cases/index.ts new file mode 100644 index 0000000000000..97e56b4220124 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/observability/cases/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Cases', function () { + loadTestFile(require.resolve('./get_case')); + loadTestFile(require.resolve('./find_cases')); + loadTestFile(require.resolve('./post_case')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/config.feature_flags.ts b/x-pack/test_serverless/api_integration/test_suites/observability/config.feature_flags.ts new file mode 100644 index 0000000000000..1e092616323f1 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/observability/config.feature_flags.ts @@ -0,0 +1,26 @@ +/* + * 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 { createTestConfig } from '../../config.base'; +import { services } from './apm_api_integration/common/services'; + +/** + * Make sure to create a MKI deployment with custom Kibana image, that includes feature flags arguments + * This tests most likely will fail on default MKI project + */ +export default createTestConfig({ + serverlessProject: 'oblt', + junit: { + reportName: 'Serverless Observability Feature Flags API Integration Tests', + }, + suiteTags: { exclude: ['skipSvlOblt'] }, + services, + // add feature flags + kbnServerArgs: ['--xpack.observability.unsafe.thresholdRule.enabled=true'], + // load tests in the index file + testFiles: [require.resolve('./index.feature_flags.ts')], +}); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/fleet.ts b/x-pack/test_serverless/api_integration/test_suites/observability/fleet/fleet.ts similarity index 95% rename from x-pack/test_serverless/api_integration/test_suites/observability/fleet.ts rename to x-pack/test_serverless/api_integration/test_suites/observability/fleet/fleet.ts index ea5ca79cbefb9..24f5e9cde9177 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/fleet.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/fleet/fleet.ts @@ -6,7 +6,7 @@ */ import expect from 'expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const svlCommonApi = getService('svlCommonApi'); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/helpers/alerting_api_helper.ts b/x-pack/test_serverless/api_integration/test_suites/observability/helpers/alerting_api_helper.ts deleted file mode 100644 index 5229cfcfc8db8..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/observability/helpers/alerting_api_helper.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MetricThresholdParams } from '@kbn/infra-plugin/common/alerting/metrics'; -import { ThresholdParams } from '@kbn/observability-plugin/common/threshold_rule/types'; -import type { SuperTest, Test } from 'supertest'; - -export async function createIndexConnector({ - supertest, - name, - indexName, -}: { - supertest: SuperTest; - name: string; - indexName: string; -}) { - const { body } = await supertest - .post(`/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo') - .send({ - name, - config: { - index: indexName, - refresh: true, - }, - connector_type_id: '.index', - }); - return body.id as string; -} - -export async function createRule({ - supertest, - name, - ruleTypeId, - params, - actions = [], - tags = [], - schedule, - consumer, -}: { - supertest: SuperTest; - ruleTypeId: string; - name: string; - params: MetricThresholdParams | ThresholdParams; - actions?: any[]; - tags?: any[]; - schedule?: { interval: string }; - consumer: string; -}) { - const { body } = await supertest - .post(`/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo') - .send({ - params, - consumer, - schedule: schedule || { - interval: '5m', - }, - tags, - name, - rule_type_id: ruleTypeId, - actions, - }); - return body; -} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/helpers/alerting_wait_for_helpers.ts b/x-pack/test_serverless/api_integration/test_suites/observability/helpers/alerting_wait_for_helpers.ts deleted file mode 100644 index fab50cdc92da3..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/observability/helpers/alerting_wait_for_helpers.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import pRetry from 'p-retry'; - -import type SuperTest from 'supertest'; -import type { Client } from '@elastic/elasticsearch'; -import type { - AggregationsAggregate, - SearchResponse, -} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -export async function waitForRuleStatus({ - id, - expectedStatus, - supertest, -}: { - id: string; - expectedStatus: string; - supertest: SuperTest.SuperTest; -}): Promise> { - return pRetry( - async () => { - const response = await supertest - .get(`/api/alerting/rule/${id}`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo'); - const { execution_status: executionStatus } = response.body || {}; - const { status } = executionStatus || {}; - if (status !== expectedStatus) { - throw new Error(`waitForStatus(${expectedStatus}): got ${status}`); - } - return executionStatus; - }, - { retries: 10 } - ); -} - -export async function waitForDocumentInIndex({ - esClient, - indexName, -}: { - esClient: Client; - indexName: string; -}): Promise>> { - return pRetry( - async () => { - const response = await esClient.search({ index: indexName }); - if (response.hits.hits.length === 0) { - throw new Error('No hits found'); - } - return response; - }, - { retries: 10 } - ); -} - -export async function waitForAlertInIndex({ - esClient, - indexName, - ruleId, -}: { - esClient: Client; - indexName: string; - ruleId: string; -}): Promise>> { - return pRetry( - async () => { - const response = await esClient.search({ - index: indexName, - body: { - query: { - term: { - 'kibana.alert.rule.uuid': ruleId, - }, - }, - }, - }); - if (response.hits.hits.length === 0) { - throw new Error('No hits found'); - } - return response; - }, - { retries: 10 } - ); -} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/helpers/data_view.ts b/x-pack/test_serverless/api_integration/test_suites/observability/helpers/data_view.ts deleted file mode 100644 index 534bc1446fd3e..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/observability/helpers/data_view.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SuperTest, Test } from 'supertest'; - -export const createDataView = async ({ - supertest, - id, - name, - title, -}: { - supertest: SuperTest; - id: string; - name: string; - title: string; -}) => { - const { body } = await supertest - .post(`/api/content_management/rpc/create`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo') - .send({ - contentTypeId: 'index-pattern', - data: { - fieldAttrs: '{}', - title, - timeFieldName: '@timestamp', - sourceFilters: '[]', - fields: '[]', - fieldFormatMap: '{}', - typeMeta: '{}', - runtimeFieldMap: '{}', - name, - }, - options: { id }, - version: 1, - }); - return body; -}; -export const deleteDataView = async ({ - supertest, - id, -}: { - supertest: SuperTest; - id: string; -}) => { - const { body } = await supertest - .post(`/api/content_management/rpc/delete`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo') - .send({ - contentTypeId: 'index-pattern', - id, - options: { force: true }, - version: 1, - }); - return body; -}; diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/index.feature_flags.ts b/x-pack/test_serverless/api_integration/test_suites/observability/index.feature_flags.ts new file mode 100644 index 0000000000000..d9643f91d70ae --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/observability/index.feature_flags.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Serverless observability API - feature flags', function () { + loadTestFile(require.resolve('./threshold_rule')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/index.ts b/x-pack/test_serverless/api_integration/test_suites/observability/index.ts index 443c9366d751b..36907484f13d3 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/index.ts @@ -8,17 +8,10 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { - describe('serverless observability API', function () { - loadTestFile(require.resolve('./fleet')); - loadTestFile(require.resolve('./snapshot_telemetry')); + describe('Serverless observability API', function () { + loadTestFile(require.resolve('./fleet/fleet')); + loadTestFile(require.resolve('./telemetry/snapshot_telemetry')); loadTestFile(require.resolve('./apm_api_integration/feature_flags.ts')); - loadTestFile(require.resolve('./threshold_rule/avg_pct_fired')); - loadTestFile(require.resolve('./threshold_rule/avg_pct_no_data')); - loadTestFile(require.resolve('./threshold_rule/documents_count_fired')); - loadTestFile(require.resolve('./threshold_rule/custom_eq_avg_bytes_fired')); - loadTestFile(require.resolve('./threshold_rule/group_by_fired')); - loadTestFile(require.resolve('./cases/post_case')); - loadTestFile(require.resolve('./cases/find_cases')); - loadTestFile(require.resolve('./cases/get_case')); + loadTestFile(require.resolve('./cases')); }); } diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/snapshot_telemetry.ts b/x-pack/test_serverless/api_integration/test_suites/observability/telemetry/snapshot_telemetry.ts similarity index 94% rename from x-pack/test_serverless/api_integration/test_suites/observability/snapshot_telemetry.ts rename to x-pack/test_serverless/api_integration/test_suites/observability/telemetry/snapshot_telemetry.ts index fa423505bf9f9..f0fc2a357156e 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/snapshot_telemetry.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/telemetry/snapshot_telemetry.ts @@ -12,8 +12,8 @@ import xpackRootTelemetrySchema from '@kbn/telemetry-collection-xpack-plugin/sch import ossPluginsTelemetrySchema from '@kbn/telemetry-plugin/schema/oss_plugins.json'; import xpackPluginsTelemetrySchema from '@kbn/telemetry-collection-xpack-plugin/schema/xpack_plugins.json'; import { assertTelemetryPayload } from '@kbn/telemetry-tools'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import type { UsageStatsPayloadTestFriendly } from '../../../../test/api_integration/services/usage_api'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import type { UsageStatsPayloadTestFriendly } from '../../../../../test/api_integration/services/usage_api'; export default function ({ getService }: FtrProviderContext) { const usageApi = getService('usageAPI'); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_fired.ts index eaf12f3c69e5f..4830f4915b37c 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_fired.ts @@ -11,14 +11,13 @@ import { FIRED_ACTIONS_ID } from '@kbn/observability-plugin/server/lib/rules/thr import expect from '@kbn/expect'; import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { createIndexConnector, createRule } from '../helpers/alerting_api_helper'; -import { createDataView, deleteDataView } from '../helpers/data_view'; export default function ({ getService }: FtrProviderContext) { const esClient = getService('es'); const supertest = getService('supertest'); const esDeleteAllIndices = getService('esDeleteAllIndices'); const alertingApi = getService('alertingApi'); + const dataViewApi = getService('dataViewApi'); const logger = getService('log'); describe('Threshold rule - AVG - PCT - FIRED', () => { @@ -31,8 +30,7 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { infraDataIndex = await generate({ esClient, lookback: 'now-15m', logger }); - await createDataView({ - supertest, + await dataViewApi.create({ name: 'metrics-fake_hosts', id: DATA_VIEW_ID, title: 'metrics-fake_hosts', @@ -56,8 +54,7 @@ export default function ({ getService }: FtrProviderContext) { index: '.kibana-event-log-*', query: { term: { 'kibana.alert.rule.consumer': 'alerts' } }, }); - await deleteDataView({ - supertest, + await dataViewApi.delete({ id: DATA_VIEW_ID, }); await esDeleteAllIndices([ALERT_ACTION_INDEX, infraDataIndex]); @@ -66,14 +63,12 @@ export default function ({ getService }: FtrProviderContext) { describe('Rule creation', () => { it('creates rule successfully', async () => { - actionId = await createIndexConnector({ - supertest, + actionId = await alertingApi.createIndexConnector({ name: 'Index Connector: Threshold API test', indexName: ALERT_ACTION_INDEX, }); - const createdRule = await createRule({ - supertest, + const createdRule = await alertingApi.createRule({ tags: ['observability'], consumer: 'alerts', name: 'Threshold rule', diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_no_data.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_no_data.ts index c101af3bc9aa8..4ff0393e273a6 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_no_data.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/avg_pct_no_data.ts @@ -10,13 +10,12 @@ import { FIRED_ACTIONS_ID } from '@kbn/observability-plugin/server/lib/rules/thr import expect from '@kbn/expect'; import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { createIndexConnector, createRule } from '../helpers/alerting_api_helper'; -import { createDataView, deleteDataView } from '../helpers/data_view'; export default function ({ getService }: FtrProviderContext) { const esClient = getService('es'); const supertest = getService('supertest'); const alertingApi = getService('alertingApi'); + const dataViewApi = getService('dataViewApi'); describe('Threshold rule - AVG - PCT - NoData', () => { const THRESHOLD_RULE_ALERT_INDEX = '.alerts-observability.threshold.alerts-default'; @@ -26,8 +25,7 @@ export default function ({ getService }: FtrProviderContext) { let ruleId: string; before(async () => { - await createDataView({ - supertest, + await dataViewApi.create({ name: 'no-data-pattern', id: DATA_VIEW_ID, title: 'no-data-pattern', @@ -51,22 +49,19 @@ export default function ({ getService }: FtrProviderContext) { index: '.kibana-event-log-*', query: { term: { 'kibana.alert.rule.consumer': 'alerts' } }, }); - await deleteDataView({ - supertest, + await dataViewApi.delete({ id: DATA_VIEW_ID, }); }); describe('Rule creation', () => { it('creates rule successfully', async () => { - actionId = await createIndexConnector({ - supertest, + actionId = await alertingApi.createIndexConnector({ name: 'Index Connector: Threshold API test', indexName: ALERT_ACTION_INDEX, }); - const createdRule = await createRule({ - supertest, + const createdRule = await alertingApi.createRule({ tags: ['observability'], consumer: 'alerts', name: 'Threshold rule', diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/custom_eq_avg_bytes_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/custom_eq_avg_bytes_fired.ts index a963245651d7a..7b2aea23f238a 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/custom_eq_avg_bytes_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/custom_eq_avg_bytes_fired.ts @@ -17,8 +17,6 @@ import { FIRED_ACTIONS_ID } from '@kbn/observability-plugin/server/lib/rules/thr import expect from '@kbn/expect'; import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { createIndexConnector, createRule } from '../helpers/alerting_api_helper'; -import { createDataView, deleteDataView } from '../helpers/data_view'; export default function ({ getService }: FtrProviderContext) { const esClient = getService('es'); @@ -26,6 +24,7 @@ export default function ({ getService }: FtrProviderContext) { const esDeleteAllIndices = getService('esDeleteAllIndices'); const logger = getService('log'); const alertingApi = getService('alertingApi'); + const dataViewApi = getService('dataViewApi'); describe('Threshold rule - CUSTOM_EQ - AVG - BYTES - FIRED', () => { const THRESHOLD_RULE_ALERT_INDEX = '.alerts-observability.threshold.alerts-default'; @@ -37,8 +36,7 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { infraDataIndex = await generate({ esClient, lookback: 'now-15m', logger }); - await createDataView({ - supertest, + await dataViewApi.create({ name: 'metrics-fake_hosts', id: DATA_VIEW_ID, title: 'metrics-fake_hosts', @@ -62,8 +60,7 @@ export default function ({ getService }: FtrProviderContext) { index: '.kibana-event-log-*', query: { term: { 'kibana.alert.rule.consumer': 'alerts' } }, }); - await deleteDataView({ - supertest, + await dataViewApi.delete({ id: DATA_VIEW_ID, }); await esDeleteAllIndices([ALERT_ACTION_INDEX, infraDataIndex]); @@ -72,14 +69,12 @@ export default function ({ getService }: FtrProviderContext) { describe('Rule creation', () => { it('creates rule successfully', async () => { - actionId = await createIndexConnector({ - supertest, + actionId = await alertingApi.createIndexConnector({ name: 'Index Connector: Threshold API test', indexName: ALERT_ACTION_INDEX, }); - const createdRule = await createRule({ - supertest, + const createdRule = await alertingApi.createRule({ tags: ['observability'], consumer: 'alerts', name: 'Threshold rule', diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/documents_count_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/documents_count_fired.ts index eacdf7b34fa4f..bd1fed6a6bd5d 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/documents_count_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/documents_count_fired.ts @@ -11,8 +11,6 @@ import { FIRED_ACTIONS_ID } from '@kbn/observability-plugin/server/lib/rules/thr import expect from '@kbn/expect'; import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { createIndexConnector, createRule } from '../helpers/alerting_api_helper'; -import { createDataView, deleteDataView } from '../helpers/data_view'; export default function ({ getService }: FtrProviderContext) { const esClient = getService('es'); @@ -20,6 +18,7 @@ export default function ({ getService }: FtrProviderContext) { const esDeleteAllIndices = getService('esDeleteAllIndices'); const logger = getService('log'); const alertingApi = getService('alertingApi'); + const dataViewApi = getService('dataViewApi'); describe('Threshold rule - DOCUMENTS_COUNT - FIRED', () => { const THRESHOLD_RULE_ALERT_INDEX = '.alerts-observability.threshold.alerts-default'; @@ -31,8 +30,7 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { infraDataIndex = await generate({ esClient, lookback: 'now-15m', logger }); - await createDataView({ - supertest, + await dataViewApi.create({ name: 'metrics-fake_hosts', id: DATA_VIEW_ID, title: 'metrics-fake_hosts', @@ -56,8 +54,7 @@ export default function ({ getService }: FtrProviderContext) { index: '.kibana-event-log-*', query: { term: { 'kibana.alert.rule.consumer': 'alerts' } }, }); - await deleteDataView({ - supertest, + await dataViewApi.delete({ id: DATA_VIEW_ID, }); await esDeleteAllIndices([ALERT_ACTION_INDEX, infraDataIndex]); @@ -66,14 +63,12 @@ export default function ({ getService }: FtrProviderContext) { describe('Rule creation', () => { it('creates rule successfully', async () => { - actionId = await createIndexConnector({ - supertest, + actionId = await alertingApi.createIndexConnector({ name: 'Index Connector: Threshold API test', indexName: ALERT_ACTION_INDEX, }); - const createdRule = await createRule({ - supertest, + const createdRule = await alertingApi.createRule({ tags: ['observability'], consumer: 'alerts', name: 'Threshold rule', diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/group_by_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/group_by_fired.ts index a78a008f93e61..244656dd97d9d 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/group_by_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/group_by_fired.ts @@ -11,6 +11,7 @@ * 2.0. */ +import { kbnTestConfig } from '@kbn/test'; import moment from 'moment'; import { cleanup, generate } from '@kbn/infra-forge'; import { Aggregators, Comparator } from '@kbn/observability-plugin/common/threshold_rule/types'; @@ -18,8 +19,6 @@ import { FIRED_ACTIONS_ID } from '@kbn/observability-plugin/server/lib/rules/thr import expect from '@kbn/expect'; import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/observability-plugin/common/constants'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { createIndexConnector, createRule } from '../helpers/alerting_api_helper'; -import { createDataView, deleteDataView } from '../helpers/data_view'; export default function ({ getService }: FtrProviderContext) { const esClient = getService('es'); @@ -27,6 +26,7 @@ export default function ({ getService }: FtrProviderContext) { const esDeleteAllIndices = getService('esDeleteAllIndices'); const logger = getService('log'); const alertingApi = getService('alertingApi'); + const dataViewApi = getService('dataViewApi'); let alertId: string; let startedAt: string; @@ -40,8 +40,7 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { infraDataIndex = await generate({ esClient, lookback: 'now-15m', logger }); - await createDataView({ - supertest, + await dataViewApi.create({ name: 'metrics-fake_hosts', id: DATA_VIEW_ID, title: 'metrics-fake_hosts', @@ -65,8 +64,7 @@ export default function ({ getService }: FtrProviderContext) { index: '.kibana-event-log-*', query: { term: { 'kibana.alert.rule.consumer': 'alerts' } }, }); - await deleteDataView({ - supertest, + await dataViewApi.delete({ id: DATA_VIEW_ID, }); await esDeleteAllIndices([ALERT_ACTION_INDEX, infraDataIndex]); @@ -75,14 +73,12 @@ export default function ({ getService }: FtrProviderContext) { describe('Rule creation', () => { it('creates rule successfully', async () => { - actionId = await createIndexConnector({ - supertest, + actionId = await alertingApi.createIndexConnector({ name: 'Index Connector: Threshold API test', indexName: ALERT_ACTION_INDEX, }); - const createdRule = await createRule({ - supertest, + const createdRule = await alertingApi.createRule({ tags: ['observability'], consumer: 'alerts', name: 'Threshold rule', @@ -217,10 +213,11 @@ export default function ({ getService }: FtrProviderContext) { }>({ indexName: ALERT_ACTION_INDEX, }); + const { protocol, hostname, port } = kbnTestConfig.getUrlParts(); expect(resp.hits.hits[0]._source?.ruleType).eql('observability.rules.threshold'); expect(resp.hits.hits[0]._source?.alertDetailsUrl).eql( - `https://localhost:5601/app/observability/alerts?_a=(kuery:%27kibana.alert.uuid:%20%22${alertId}%22%27%2CrangeFrom:%27${rangeFrom}%27%2CrangeTo:now%2Cstatus:all)` + `${protocol}://${hostname}:${port}/app/observability/alerts?_a=(kuery:%27kibana.alert.uuid:%20%22${alertId}%22%27%2CrangeFrom:%27${rangeFrom}%27%2CrangeTo:now%2Cstatus:all)` ); expect(resp.hits.hits[0]._source?.reason).eql( 'Custom equation is 0.8 in the last 1 min for host-0. Alert when >= 0.2.' diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/index.ts b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/index.ts new file mode 100644 index 0000000000000..dbb8968d2d946 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Threshold Rule', function () { + loadTestFile(require.resolve('./avg_pct_fired')); + loadTestFile(require.resolve('./avg_pct_no_data')); + loadTestFile(require.resolve('./documents_count_fired')); + loadTestFile(require.resolve('./custom_eq_avg_bytes_fired')); + loadTestFile(require.resolve('./group_by_fired')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/search/config.feature_flags.ts b/x-pack/test_serverless/api_integration/test_suites/search/config.feature_flags.ts new file mode 100644 index 0000000000000..9a9d0064bc5e6 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/search/config.feature_flags.ts @@ -0,0 +1,24 @@ +/* + * 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 { createTestConfig } from '../../config.base'; + +/** + * Make sure to create a MKI deployment with custom Kibana image, that includes feature flags arguments + * This tests most likely will fail on default MKI project + */ +export default createTestConfig({ + serverlessProject: 'es', + junit: { + reportName: 'Serverless Search Feature Flags API Integration Tests', + }, + suiteTags: { exclude: ['skipSvlSearch'] }, + // add feature flags + kbnServerArgs: [], + // load tests in the index file + testFiles: [require.resolve('./index.feature_flags.ts')], +}); diff --git a/x-pack/test_serverless/api_integration/test_suites/search/index.feature_flags.ts b/x-pack/test_serverless/api_integration/test_suites/search/index.feature_flags.ts new file mode 100644 index 0000000000000..d388b06cef577 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/search/index.feature_flags.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export default function () { + describe('Serverless search API - feature flags', function () { + // add tests that require feature flags, defined in config.feature_flags.ts + // loadTestFile(require.resolve()); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/search/index.ts b/x-pack/test_serverless/api_integration/test_suites/search/index.ts index 13ddf80d5a950..78964aa73c786 100644 --- a/x-pack/test_serverless/api_integration/test_suites/search/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/search/index.ts @@ -8,9 +8,9 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { - describe('serverless search API', function () { - loadTestFile(require.resolve('./snapshot_telemetry')); - loadTestFile(require.resolve('./cases/post_case')); + describe('Serverless search API', function () { + loadTestFile(require.resolve('./telemetry/snapshot_telemetry')); loadTestFile(require.resolve('./cases/find_cases')); + loadTestFile(require.resolve('./cases/post_case')); }); } diff --git a/x-pack/test_serverless/api_integration/test_suites/search/snapshot_telemetry.ts b/x-pack/test_serverless/api_integration/test_suites/search/telemetry/snapshot_telemetry.ts similarity index 94% rename from x-pack/test_serverless/api_integration/test_suites/search/snapshot_telemetry.ts rename to x-pack/test_serverless/api_integration/test_suites/search/telemetry/snapshot_telemetry.ts index 39608bba1fe62..fd4893d5e9e49 100644 --- a/x-pack/test_serverless/api_integration/test_suites/search/snapshot_telemetry.ts +++ b/x-pack/test_serverless/api_integration/test_suites/search/telemetry/snapshot_telemetry.ts @@ -12,8 +12,8 @@ import ossRootTelemetrySchema from '@kbn/telemetry-plugin/schema/oss_root.json'; import ossPluginsTelemetrySchema from '@kbn/telemetry-plugin/schema/oss_plugins.json'; import xpackRootTelemetrySchema from '@kbn/telemetry-collection-xpack-plugin/schema/xpack_root.json'; import xpackPluginsTelemetrySchema from '@kbn/telemetry-collection-xpack-plugin/schema/xpack_plugins.json'; -import type { FtrProviderContext } from '../../ftr_provider_context'; -import type { UsageStatsPayloadTestFriendly } from '../../../../test/api_integration/services/usage_api'; +import type { FtrProviderContext } from '../../../ftr_provider_context'; +import type { UsageStatsPayloadTestFriendly } from '../../../../../test/api_integration/services/usage_api'; export default function ({ getService }: FtrProviderContext) { const usageApi = getService('usageAPI'); diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cases/index.ts b/x-pack/test_serverless/api_integration/test_suites/security/cases/index.ts new file mode 100644 index 0000000000000..97e56b4220124 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/security/cases/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Cases', function () { + loadTestFile(require.resolve('./get_case')); + loadTestFile(require.resolve('./find_cases')); + loadTestFile(require.resolve('./post_case')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/security/config.feature_flags.ts b/x-pack/test_serverless/api_integration/test_suites/security/config.feature_flags.ts new file mode 100644 index 0000000000000..20bce40a9f205 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/security/config.feature_flags.ts @@ -0,0 +1,24 @@ +/* + * 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 { createTestConfig } from '../../config.base'; + +/** + * Make sure to create a MKI deployment with custom Kibana image, that includes feature flags arguments + * This tests most likely will fail on default MKI project + */ +export default createTestConfig({ + serverlessProject: 'security', + junit: { + reportName: 'Serverless Security Feature Flags API Integration Tests', + }, + suiteTags: { exclude: ['skipSvlSec'] }, + // add feature flags + kbnServerArgs: [], + // load tests in the index file + testFiles: [require.resolve('./index.feature_flags.ts')], +}); diff --git a/x-pack/test_serverless/api_integration/test_suites/security/fleet.ts b/x-pack/test_serverless/api_integration/test_suites/security/fleet/fleet.ts similarity index 95% rename from x-pack/test_serverless/api_integration/test_suites/security/fleet.ts rename to x-pack/test_serverless/api_integration/test_suites/security/fleet/fleet.ts index ea5ca79cbefb9..24f5e9cde9177 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/fleet.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/fleet/fleet.ts @@ -6,7 +6,7 @@ */ import expect from 'expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const svlCommonApi = getService('svlCommonApi'); diff --git a/x-pack/test_serverless/api_integration/test_suites/security/index.feature_flags.ts b/x-pack/test_serverless/api_integration/test_suites/security/index.feature_flags.ts new file mode 100644 index 0000000000000..ef8d2a7a43844 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/security/index.feature_flags.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export default function () { + describe('Serverless security API - feature flags', function () { + // add tests that require feature flags, defined in config.feature_flags.ts + // loadTestFile(require.resolve()); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/security/index.ts b/x-pack/test_serverless/api_integration/test_suites/security/index.ts index 294f4b32af5e6..eaf193c5f659c 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/index.ts @@ -8,11 +8,9 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { - describe('serverless security API', function () { - loadTestFile(require.resolve('./fleet')); - loadTestFile(require.resolve('./snapshot_telemetry')); - loadTestFile(require.resolve('./cases/post_case')); - loadTestFile(require.resolve('./cases/find_cases')); - loadTestFile(require.resolve('./cases/get_case')); + describe('Serverless security API', function () { + loadTestFile(require.resolve('./telemetry/snapshot_telemetry')); + loadTestFile(require.resolve('./fleet/fleet')); + loadTestFile(require.resolve('./cases')); }); } diff --git a/x-pack/test_serverless/api_integration/test_suites/security/snapshot_telemetry.ts b/x-pack/test_serverless/api_integration/test_suites/security/telemetry/snapshot_telemetry.ts similarity index 94% rename from x-pack/test_serverless/api_integration/test_suites/security/snapshot_telemetry.ts rename to x-pack/test_serverless/api_integration/test_suites/security/telemetry/snapshot_telemetry.ts index 7d6e5abb2e063..1fdc341454e3c 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/snapshot_telemetry.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/telemetry/snapshot_telemetry.ts @@ -12,8 +12,8 @@ import xpackRootTelemetrySchema from '@kbn/telemetry-collection-xpack-plugin/sch import ossPluginsTelemetrySchema from '@kbn/telemetry-plugin/schema/oss_plugins.json'; import xpackPluginsTelemetrySchema from '@kbn/telemetry-collection-xpack-plugin/schema/xpack_plugins.json'; import { assertTelemetryPayload } from '@kbn/telemetry-tools'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import type { UsageStatsPayloadTestFriendly } from '../../../../test/api_integration/services/usage_api'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import type { UsageStatsPayloadTestFriendly } from '../../../../../test/api_integration/services/usage_api'; export default function ({ getService }: FtrProviderContext) { const usageApi = getService('usageAPI'); diff --git a/x-pack/test_serverless/functional/test_suites/observability/config.feature_flags.ts b/x-pack/test_serverless/functional/test_suites/observability/config.feature_flags.ts new file mode 100644 index 0000000000000..31995ad616ca4 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/config.feature_flags.ts @@ -0,0 +1,24 @@ +/* + * 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 { createTestConfig } from '../../config.base'; + +/** + * Make sure to create a MKI deployment with custom Kibana image, that includes feature flags arguments + * This tests most likely will fail on default MKI project + */ +export default createTestConfig({ + serverlessProject: 'oblt', + junit: { + reportName: 'Serverless Observability Feature Flags Functional Tests', + }, + suiteTags: { exclude: ['skipSvlOblt'] }, + // add feature flags + kbnServerArgs: [], + // load tests in the index file + testFiles: [require.resolve('./index.feature_flags.ts')], +}); diff --git a/x-pack/test_serverless/functional/test_suites/observability/index.feature_flags.ts b/x-pack/test_serverless/functional/test_suites/observability/index.feature_flags.ts new file mode 100644 index 0000000000000..1212d6a734403 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/index.feature_flags.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export default function () { + describe('serverless observability UI - feature flags', function () { + // add tests that require feature flags, defined in config.feature_flags.ts + // loadTestFile(require.resolve()); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/search/config.feature_flags.ts b/x-pack/test_serverless/functional/test_suites/search/config.feature_flags.ts new file mode 100644 index 0000000000000..e93c3ff2f02e5 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/search/config.feature_flags.ts @@ -0,0 +1,24 @@ +/* + * 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 { createTestConfig } from '../../config.base'; + +/** + * Make sure to create a MKI deployment with custom Kibana image, that includes feature flags arguments + * This tests most likely will fail on default MKI project + */ +export default createTestConfig({ + serverlessProject: 'es', + junit: { + reportName: 'Serverless Search Feature Flags Functional Tests', + }, + suiteTags: { exclude: ['skipSvlSearch'] }, + // add feature flags + kbnServerArgs: [], + // load tests in the index file + testFiles: [require.resolve('./index.feature_flags.ts')], +}); diff --git a/x-pack/test_serverless/functional/test_suites/search/index.feature_flags.ts b/x-pack/test_serverless/functional/test_suites/search/index.feature_flags.ts new file mode 100644 index 0000000000000..a609860a6a746 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/search/index.feature_flags.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export default function () { + describe('serverless search UI - feature flags', function () { + // add tests that require feature flags, defined in config.feature_flags.ts + // loadTestFile(require.resolve()); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/security/config.feature_flags.ts b/x-pack/test_serverless/functional/test_suites/security/config.feature_flags.ts new file mode 100644 index 0000000000000..735c8a8765d16 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/config.feature_flags.ts @@ -0,0 +1,24 @@ +/* + * 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 { createTestConfig } from '../../config.base'; + +/** + * Make sure to create a MKI deployment with custom Kibana image, that includes feature flags arguments + * This tests most likely will fail on default MKI project + */ +export default createTestConfig({ + serverlessProject: 'security', + junit: { + reportName: 'Serverless Security Feature Flags Functional Tests', + }, + suiteTags: { exclude: ['skipSvlSec'] }, + // add feature flags + kbnServerArgs: [], + // load tests in the index file + testFiles: [require.resolve('./index.feature_flags.ts')], +}); diff --git a/x-pack/test_serverless/functional/test_suites/security/index.feature_flags.ts b/x-pack/test_serverless/functional/test_suites/security/index.feature_flags.ts new file mode 100644 index 0000000000000..8e28b53c18b22 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/index.feature_flags.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export default function () { + describe('serverless security UI - feature flags', function () { + // add tests that require feature flags, defined in config.feature_flags.ts + // loadTestFile(require.resolve()); + }); +} diff --git a/x-pack/test_serverless/shared/config.base.ts b/x-pack/test_serverless/shared/config.base.ts index 73949573038f3..d9d34a1e14198 100644 --- a/x-pack/test_serverless/shared/config.base.ts +++ b/x-pack/test_serverless/shared/config.base.ts @@ -91,6 +91,7 @@ export default async () => { saml: { 'cloud-saml-kibana': { order: 1, realm: 'cloud-saml-kibana' } }, })}`, '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', + `--server.publicBaseUrl=${servers.kibana.protocol}://${servers.kibana.hostname}:${servers.kibana.port}`, ], }, diff --git a/x-pack/test_serverless/shared/types/index.ts b/x-pack/test_serverless/shared/types/index.ts index 8dfb1a978699d..0a36e71db7c39 100644 --- a/x-pack/test_serverless/shared/types/index.ts +++ b/x-pack/test_serverless/shared/types/index.ts @@ -9,6 +9,7 @@ import { InheritedServices } from '../../api_integration/services'; export interface CreateTestConfigOptions { serverlessProject: 'es' | 'oblt' | 'security'; + kbnServerArgs?: string[]; testFiles: string[]; junit: { reportName: string }; suiteTags?: { include?: string[]; exclude?: string[] };