From 546a3cb1afa376048ba90372dac23a50c6e4a84d Mon Sep 17 00:00:00 2001
From: Dima Arnautov
Date: Tue, 22 Aug 2023 14:01:34 +0200
Subject: [PATCH 01/26] [ML] AIOps: Update assertion for the change point
detection tests (#164404)
## Summary
Closes https://github.com/elastic/kibana/issues/158851
Updates assertion for the split field test.
---
x-pack/test/functional/apps/aiops/change_point_detection.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
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));
});
From 7ece4e5df3e6f8934c6ad1f8b462762431efeb9f Mon Sep 17 00:00:00 2001
From: Dzmitry Lemechko
Date: Tue, 22 Aug 2023 14:09:41 +0200
Subject: [PATCH 02/26] [FTR] Move serverless tests depending on feature flag
to its own config file (#163929)
## Summary
It seems beneficial to have feature flag tests in a separate test config
file
- tests are still run on Kibana CI automatically
- tests are not run on MKI projects automatically, but you can deploy
custom project and run tests via feature flags config
All the feature flags within the same project should be places in the
same config to make sure there is no arguments conflict.
When the flag is moved to the yml configuration, we can rely on Kibana
CI and manually triggered deployment to make sure projects are
functioning correctly.
---------
Co-authored-by: Robert Oskamp
---
.buildkite/ftr_configs.yml | 6 ++
.../steps/functional/serverless_ftr.sh | 3 +
x-pack/test_serverless/README.md | 27 ++++++
.../api_integration/config.base.ts | 4 +-
.../api_integration/services/alerting_api.ts | 54 +++++++++++
.../api_integration/services/data_view_api.ts | 52 +++++++++++
.../api_integration/services/index.ts | 2 +
.../test_suites/observability/cases/index.ts | 16 ++++
.../observability/config.feature_flags.ts | 26 ++++++
.../observability/{ => fleet}/fleet.ts | 2 +-
.../helpers/alerting_api_helper.ts | 71 ---------------
.../helpers/alerting_wait_for_helpers.ts | 90 -------------------
.../observability/helpers/data_view.ts | 61 -------------
.../observability/index.feature_flags.ts | 14 +++
.../test_suites/observability/index.ts | 15 +---
.../{ => telemetry}/snapshot_telemetry.ts | 4 +-
.../threshold_rule/avg_pct_fired.ts | 15 ++--
.../threshold_rule/avg_pct_no_data.ts | 15 ++--
.../custom_eq_avg_bytes_fired.ts | 15 ++--
.../threshold_rule/documents_count_fired.ts | 15 ++--
.../threshold_rule/group_by_fired.ts | 19 ++--
.../observability/threshold_rule/index.ts | 18 ++++
.../search/config.feature_flags.ts | 24 +++++
.../test_suites/search/index.feature_flags.ts | 13 +++
.../test_suites/search/index.ts | 6 +-
.../{ => telemetry}/snapshot_telemetry.ts | 4 +-
.../test_suites/security/cases/index.ts | 16 ++++
.../security/config.feature_flags.ts | 24 +++++
.../test_suites/security/{ => fleet}/fleet.ts | 2 +-
.../security/index.feature_flags.ts | 13 +++
.../test_suites/security/index.ts | 10 +--
.../{ => telemetry}/snapshot_telemetry.ts | 4 +-
.../observability/config.feature_flags.ts | 24 +++++
.../observability/index.feature_flags.ts | 13 +++
.../search/config.feature_flags.ts | 24 +++++
.../test_suites/search/index.feature_flags.ts | 13 +++
.../security/config.feature_flags.ts | 24 +++++
.../security/index.feature_flags.ts | 13 +++
x-pack/test_serverless/shared/config.base.ts | 1 +
x-pack/test_serverless/shared/types/index.ts | 1 +
40 files changed, 469 insertions(+), 304 deletions(-)
create mode 100644 x-pack/test_serverless/api_integration/services/data_view_api.ts
create mode 100644 x-pack/test_serverless/api_integration/test_suites/observability/cases/index.ts
create mode 100644 x-pack/test_serverless/api_integration/test_suites/observability/config.feature_flags.ts
rename x-pack/test_serverless/api_integration/test_suites/observability/{ => fleet}/fleet.ts (95%)
delete mode 100644 x-pack/test_serverless/api_integration/test_suites/observability/helpers/alerting_api_helper.ts
delete mode 100644 x-pack/test_serverless/api_integration/test_suites/observability/helpers/alerting_wait_for_helpers.ts
delete mode 100644 x-pack/test_serverless/api_integration/test_suites/observability/helpers/data_view.ts
create mode 100644 x-pack/test_serverless/api_integration/test_suites/observability/index.feature_flags.ts
rename x-pack/test_serverless/api_integration/test_suites/observability/{ => telemetry}/snapshot_telemetry.ts (94%)
create mode 100644 x-pack/test_serverless/api_integration/test_suites/observability/threshold_rule/index.ts
create mode 100644 x-pack/test_serverless/api_integration/test_suites/search/config.feature_flags.ts
create mode 100644 x-pack/test_serverless/api_integration/test_suites/search/index.feature_flags.ts
rename x-pack/test_serverless/api_integration/test_suites/search/{ => telemetry}/snapshot_telemetry.ts (94%)
create mode 100644 x-pack/test_serverless/api_integration/test_suites/security/cases/index.ts
create mode 100644 x-pack/test_serverless/api_integration/test_suites/security/config.feature_flags.ts
rename x-pack/test_serverless/api_integration/test_suites/security/{ => fleet}/fleet.ts (95%)
create mode 100644 x-pack/test_serverless/api_integration/test_suites/security/index.feature_flags.ts
rename x-pack/test_serverless/api_integration/test_suites/security/{ => telemetry}/snapshot_telemetry.ts (94%)
create mode 100644 x-pack/test_serverless/functional/test_suites/observability/config.feature_flags.ts
create mode 100644 x-pack/test_serverless/functional/test_suites/observability/index.feature_flags.ts
create mode 100644 x-pack/test_serverless/functional/test_suites/search/config.feature_flags.ts
create mode 100644 x-pack/test_serverless/functional/test_suites/search/index.feature_flags.ts
create mode 100644 x-pack/test_serverless/functional/test_suites/security/config.feature_flags.ts
create mode 100644 x-pack/test_serverless/functional/test_suites/security/index.feature_flags.ts
diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml
index 616f32285c99a..b26b54b767658 100644
--- a/.buildkite/ftr_configs.yml
+++ b/.buildkite/ftr_configs.yml
@@ -81,12 +81,18 @@ disabled:
# Serverless configs, currently only for manual tests runs, CI integration planned
- x-pack/test_serverless/api_integration/test_suites/common/config.ts
- x-pack/test_serverless/api_integration/test_suites/observability/config.ts
+ - x-pack/test_serverless/api_integration/test_suites/observability/config.feature_flags.ts
- x-pack/test_serverless/api_integration/test_suites/search/config.ts
+ - x-pack/test_serverless/api_integration/test_suites/search/config.feature_flags.ts
- x-pack/test_serverless/api_integration/test_suites/security/config.ts
+ - x-pack/test_serverless/api_integration/test_suites/security/config.feature_flags.ts
- x-pack/test_serverless/functional/test_suites/common/config.ts
- x-pack/test_serverless/functional/test_suites/observability/config.ts
+ - x-pack/test_serverless/functional/test_suites/observability/config.feature_flags.ts
- x-pack/test_serverless/functional/test_suites/search/config.ts
+ - x-pack/test_serverless/functional/test_suites/search/config.feature_flags.ts
- x-pack/test_serverless/functional/test_suites/security/config.ts
+ - x-pack/test_serverless/functional/test_suites/security/config.feature_flags.ts
defaultQueue: 'n2-4-spot'
enabled:
diff --git a/.buildkite/scripts/steps/functional/serverless_ftr.sh b/.buildkite/scripts/steps/functional/serverless_ftr.sh
index 51eee3307e0b3..ba75fe6034cae 100755
--- a/.buildkite/scripts/steps/functional/serverless_ftr.sh
+++ b/.buildkite/scripts/steps/functional/serverless_ftr.sh
@@ -9,17 +9,20 @@ export JOB="kibana-serverless-$SERVERLESS_ENVIRONMENT"
if [[ "$SERVERLESS_ENVIRONMENT" == "search" ]]; then
SERVERLESS_CONFIGS=(
"x-pack/test_serverless/api_integration/test_suites/search/config.ts"
+ "x-pack/test_serverless/api_integration/test_suites/search/config.feature_flags.ts"
"x-pack/test_serverless/functional/test_suites/search/config.ts"
)
elif [[ "$SERVERLESS_ENVIRONMENT" == "observability" ]]; then
SERVERLESS_CONFIGS=(
"x-pack/test_serverless/api_integration/test_suites/observability/config.ts"
+ "x-pack/test_serverless/api_integration/test_suites/observability/config.feature_flags.ts"
"x-pack/test_serverless/functional/test_suites/observability/config.ts"
"x-pack/test_serverless/functional/test_suites/observability/cypress/config_headless.ts"
)
elif [[ "$SERVERLESS_ENVIRONMENT" == "security" ]]; then
SERVERLESS_CONFIGS=(
"x-pack/test_serverless/api_integration/test_suites/security/config.ts"
+ "x-pack/test_serverless/api_integration/test_suites/security/config.feature_flags.ts"
"x-pack/test_serverless/functional/test_suites/security/config.ts"
)
fi
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 bab7b001b5982..8102eeb9f4c1b 100644
--- a/x-pack/test_serverless/api_integration/services/index.ts
+++ b/x-pack/test_serverless/api_integration/services/index.ts
@@ -12,6 +12,7 @@ import { services as svlSharedServices } from '../../shared/services';
import { SvlCommonApiServiceProvider } from './svl_common_api';
import { AlertingApiProvider } from './alerting_api';
+import { DataViewApiProvider } from './data_view_api';
export const services = {
...xpackApiIntegrationServices,
@@ -19,6 +20,7 @@ export const services = {
svlCommonApi: SvlCommonApiServiceProvider,
alertingApi: AlertingApiProvider,
+ dataViewApi: DataViewApiProvider,
};
export type InheritedFtrProviderContext = GenericFtrProviderContext;
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 35c4320a7f17f..3cd2f21310809 100644
--- a/x-pack/test_serverless/shared/config.base.ts
+++ b/x-pack/test_serverless/shared/config.base.ts
@@ -61,6 +61,7 @@ export default async () => {
},
])}`,
'--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[] };
From 635eedb31ba6a3ac1ac90e29ce9350a6b6f3ac64 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Loix?=
Date: Tue, 22 Aug 2023 13:13:48 +0100
Subject: [PATCH 03/26] [SavedObject tagging] Fix functional test for bulk
delete tags (#164321)
---
.../test/saved_object_tagging/functional/tests/bulk_actions.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
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']);
});
});
From 5eeb3f7c47a2a8f18ec08f36cbbd71ff8724eea6 Mon Sep 17 00:00:00 2001
From: GitStart <1501599+gitstart@users.noreply.github.com>
Date: Tue, 22 Aug 2023 13:14:38 +0100
Subject: [PATCH 04/26] [ILM] Migrate all usages of EuiPage*_Deprecated
(#163132)
---
.../__snapshots__/policy_table.test.tsx.snap | 91 ++++++++++---------
.../edit_policy/edit_policy.container.tsx | 76 +++++++---------
.../policy_list/policy_list.container.tsx | 76 +++++++---------
.../sections/policy_list/policy_list.tsx | 50 +++++-----
4 files changed, 136 insertions(+), 157 deletions(-)
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap
index 6c991799a115b..aeae7b28f0b88 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap
@@ -37,68 +37,71 @@ Array [
`;
exports[`policy table shows empty state when there are no policies 1`] = `
-
-
-
-
-
- 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}
+ />
);
}
From 58334b54ac199744cae16d4b9f690dcb01ae6b88 Mon Sep 17 00:00:00 2001
From: Isaac Karrer
Date: Tue, 22 Aug 2023 07:30:08 -0500
Subject: [PATCH 05/26] Kibana QualityGate Scaffold (#163831)
The main question is here did I get the github team names correct for
fleet and security? Are there any other groups I am missing who will
have quality gates they want to see execute on every kibana release?
- The PR which registered the repo and controls who can write secrets to
vault was merged
[here](https://github.com/elastic/catalog-info/pull/488/files)
- Quality gate registration with argo deploy pipeline
[here](https://github.com/elastic/serverless-gitops/pull/586). Should
merge after this and pipelines are working.
- [this one](https://github.com/elastic/catalog-info/pull/485/files)
merges last and tells terrazzo about the catalog-info.yaml file with our
pipeline. Other pipelines should be migrated over but are not included
here.
Rel: https://github.com/elastic/ingest-dev/issues/2201
Rel: https://elasticco.atlassian.net/browse/QX-282
---------
Co-authored-by: Ramon Butter
---
.../quality-gates/pipeline.kibana-tests.yaml | 10 +++
.../pipeline.tests-production.yaml | 15 ++++
.../quality-gates/pipeline.tests-qa.yaml | 15 ++++
.../quality-gates/pipeline.tests-staging.yaml | 15 ++++
.github/CODEOWNERS | 1 +
catalog-info.yaml | 70 +++++++++++++++++++
src/dev/precommit_hook/casing_check_config.js | 3 +
7 files changed, 129 insertions(+)
create mode 100644 .buildkite/pipelines/quality-gates/pipeline.kibana-tests.yaml
create mode 100644 .buildkite/pipelines/quality-gates/pipeline.tests-production.yaml
create mode 100644 .buildkite/pipelines/quality-gates/pipeline.tests-qa.yaml
create mode 100644 .buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml
create mode 100644 catalog-info.yaml
diff --git a/.buildkite/pipelines/quality-gates/pipeline.kibana-tests.yaml b/.buildkite/pipelines/quality-gates/pipeline.kibana-tests.yaml
new file mode 100644
index 0000000000000..27e55dfced9d7
--- /dev/null
+++ b/.buildkite/pipelines/quality-gates/pipeline.kibana-tests.yaml
@@ -0,0 +1,10 @@
+env:
+ ENVIRONMENT: ${ENVIRONMENT?}
+
+steps:
+ - label: ":pipeline::grey_question::seedling: Trigger Kibana Tests for ${ENVIRONMENT}"
+ env:
+ QG_PIPELINE_LOCATION: ".buildkite/pipelines/quality-gates"
+ command: "make -C /agent run-environment-tests"
+ agents:
+ image: "docker.elastic.co/ci-agent-images/quality-gate-seedling:0.0.2"
diff --git a/.buildkite/pipelines/quality-gates/pipeline.tests-production.yaml b/.buildkite/pipelines/quality-gates/pipeline.tests-production.yaml
new file mode 100644
index 0000000000000..483532b9c7435
--- /dev/null
+++ b/.buildkite/pipelines/quality-gates/pipeline.tests-production.yaml
@@ -0,0 +1,15 @@
+steps:
+ - label: ":pipeline::kibana::seedling: Trigger Kibana Tests for ${ENVIRONMENT}"
+ command: echo "replace me with Kibana specific tests"
+ agent:
+ image: "docker.elastic.co/ci-agent-images/basic-buildkite-agent:1688566364"
+
+ - label: ":pipeline::fleet::seedling: Trigger Fleet Kibana Tests for ${ENVIRONMENT}"
+ command: echo "replace me with Fleet specific Kibana tests"
+ agent:
+ image: "docker.elastic.co/ci-agent-images/basic-buildkite-agent:1688566364"
+
+ - label: ":pipeline::lock::seedling: Trigger Security Kibana Tests for ${ENVIRONMENT}"
+ command: echo "replace me with Security specific Kibana tests"
+ agent:
+ image: "docker.elastic.co/ci-agent-images/basic-buildkite-agent:1688566364"
diff --git a/.buildkite/pipelines/quality-gates/pipeline.tests-qa.yaml b/.buildkite/pipelines/quality-gates/pipeline.tests-qa.yaml
new file mode 100644
index 0000000000000..483532b9c7435
--- /dev/null
+++ b/.buildkite/pipelines/quality-gates/pipeline.tests-qa.yaml
@@ -0,0 +1,15 @@
+steps:
+ - label: ":pipeline::kibana::seedling: Trigger Kibana Tests for ${ENVIRONMENT}"
+ command: echo "replace me with Kibana specific tests"
+ agent:
+ image: "docker.elastic.co/ci-agent-images/basic-buildkite-agent:1688566364"
+
+ - label: ":pipeline::fleet::seedling: Trigger Fleet Kibana Tests for ${ENVIRONMENT}"
+ command: echo "replace me with Fleet specific Kibana tests"
+ agent:
+ image: "docker.elastic.co/ci-agent-images/basic-buildkite-agent:1688566364"
+
+ - label: ":pipeline::lock::seedling: Trigger Security Kibana Tests for ${ENVIRONMENT}"
+ command: echo "replace me with Security specific Kibana tests"
+ agent:
+ image: "docker.elastic.co/ci-agent-images/basic-buildkite-agent:1688566364"
diff --git a/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml b/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml
new file mode 100644
index 0000000000000..483532b9c7435
--- /dev/null
+++ b/.buildkite/pipelines/quality-gates/pipeline.tests-staging.yaml
@@ -0,0 +1,15 @@
+steps:
+ - label: ":pipeline::kibana::seedling: Trigger Kibana Tests for ${ENVIRONMENT}"
+ command: echo "replace me with Kibana specific tests"
+ agent:
+ image: "docker.elastic.co/ci-agent-images/basic-buildkite-agent:1688566364"
+
+ - label: ":pipeline::fleet::seedling: Trigger Fleet Kibana Tests for ${ENVIRONMENT}"
+ command: echo "replace me with Fleet specific Kibana tests"
+ agent:
+ image: "docker.elastic.co/ci-agent-images/basic-buildkite-agent:1688566364"
+
+ - label: ":pipeline::lock::seedling: Trigger Security Kibana Tests for ${ENVIRONMENT}"
+ command: echo "replace me with Security specific Kibana tests"
+ agent:
+ image: "docker.elastic.co/ci-agent-images/basic-buildkite-agent:1688566364"
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 650c71e26c93b..eecffc061f483 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -988,6 +988,7 @@ x-pack/plugins/infra/server/lib/alerting @elastic/actionable-observability
/.buildkite/ @elastic/kibana-operations
/kbn_pm/ @elastic/kibana-operations
/x-pack/dev-tools @elastic/kibana-operations
+catalog-info.yaml @elastic/kibana-operations @elastic/kibana-tech-leads
# Appex QA
/src/dev/code_coverage @elastic/appex-qa
diff --git a/catalog-info.yaml b/catalog-info.yaml
new file mode 100644
index 0000000000000..534e34ba27008
--- /dev/null
+++ b/catalog-info.yaml
@@ -0,0 +1,70 @@
+---
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: kibana
+ description: Kibana is a user interface that lets you visualize your Elasticsearch data and navigate the Elastic Stack.
+
+ annotations:
+ backstage.io/source-location: url:https://github.com/elastic/kibana/tree/main
+ github.com/project-slug: elastic/kibana
+ github.com/team-slug: elastic/kibana-tech-leads
+ buildkite.com/project-slug: elastic/kibana
+
+ tags:
+ - typescript
+ - javascript
+ - dashboards
+ - metrics
+ - visualizations
+ - observability
+
+ links:
+ - title: Documentation
+ url: https://www.elastic.co/guide/en/kibana/current/index.html
+
+spec:
+ type: monorepo
+ owner: group:kibana-tech-leads
+ lifecycle: production
+
+---
+
+apiVersion: backstage.io/v1alpha1
+kind: Resource
+metadata:
+ name: kibana-tests-pipeline
+ description: Definition of the kibana pipeline
+ links:
+ - title: Pipeline
+ url: https://buildkite.com/elastic/kibana-tests
+spec:
+ type: buildkite-pipeline
+ owner: group:kibana-tech-leads
+ system: buildkite
+ implementation:
+ apiVersion: buildkite.elastic.dev/v1
+ kind: Pipeline
+ metadata:
+ name: kibana-tests
+ description: Pipeline that tests the service integration in various environments
+ spec:
+ repository: elastic/kibana
+ pipeline_file: ./.buildkite/pipelines/pipeline.kibana-tests.yaml
+ provider_settings:
+ trigger_mode: none
+ teams:
+ kibana-operations:
+ access_level: MANAGE_BUILD_AND_READ
+ security-engineering-productivity:
+ access_level: BUILD_AND_READ
+ fleet:
+ access_level: BUILD_AND_READ
+ kibana-tech-leads:
+ access_level: BUILD_AND_READ
+ kibana-core:
+ access_level: BUILD_AND_READ
+ cloud-tooling:
+ access_level: BUILD_AND_READ
+ everyone:
+ access_level: READ_ONLY
diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js
index c3b1a27491e67..93eba1bb171b2 100644
--- a/src/dev/precommit_hook/casing_check_config.js
+++ b/src/dev/precommit_hook/casing_check_config.js
@@ -50,6 +50,9 @@ export const IGNORE_FILE_GLOBS = [
// Required to match the name in the docs.elastic.dev repo.
'nav-kibana-dev.docnav.json',
+ // Match elastic wide naming convention for catalog-info.yaml
+ 'catalog-info.yaml',
+
// filename must match language code which requires capital letters
'**/translations/*.json',
From 83a0eecf25bd6cb5adf71251ceb81ef8ef429ba5 Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Tue, 22 Aug 2023 13:53:50 +0100
Subject: [PATCH 06/26] skip flaky suite (#164381)
---
.../apps/ml/anomaly_detection_result_views/forecasts.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
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');
From dd8239fdaf841b23bb89b5e7b4b41606846501f0 Mon Sep 17 00:00:00 2001
From: Saikat Sarkar <132922331+saikatsarkar056@users.noreply.github.com>
Date: Tue, 22 Aug 2023 07:09:40 -0600
Subject: [PATCH 07/26] [Search Relevance] Move error handler to selector
(#164113)
This change is related to this issue:
https://github.com/elastic/enterprise-search-team/issues/4440
In this PR, we are moving error handler to selectors.
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../text_expansion_callout.test.tsx | 14 ++----
.../ml_inference/text_expansion_callout.tsx | 14 ++----
.../text_expansion_callout_logic.test.ts | 44 +++++++++++++++++++
.../text_expansion_callout_logic.ts | 23 ++++++++++
4 files changed, 74 insertions(+), 21 deletions(-)
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout.test.tsx
index a1ee2410128c1..1ef7480b25c81 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout.test.tsx
@@ -11,8 +11,6 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { HttpError } from '../../../../../../../common/types/api';
-
import { DeployModel } from './deploy_model';
import { ModelDeployed } from './model_deployed';
import { ModelDeploymentInProgress } from './model_deployment_in_progress';
@@ -31,7 +29,6 @@ jest.mock('./text_expansion_callout_data', () => ({
}));
const DEFAULT_VALUES = {
- startTextExpansionModelError: undefined,
isCreateButtonDisabled: false,
isModelDownloadInProgress: false,
isModelDownloaded: false,
@@ -47,13 +44,10 @@ describe('TextExpansionCallOut', () => {
it('renders error panel instead of normal panel if there are some errors', () => {
setMockValues({
...DEFAULT_VALUES,
- startTextExpansionModelError: {
- body: {
- error: 'some-error',
- message: 'some-error-message',
- statusCode: 500,
- },
- } as HttpError,
+ textExpansionError: {
+ title: 'Error with ELSER deployment',
+ message: 'Mocked error message',
+ },
});
const wrapper = shallow();
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout.tsx
index 7ded5c3e9035d..310f8f273a2cb 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout.tsx
@@ -20,7 +20,7 @@ import { ModelDeployed } from './model_deployed';
import { ModelDeploymentInProgress } from './model_deployment_in_progress';
import { ModelStarted } from './model_started';
import { useTextExpansionCallOutData } from './text_expansion_callout_data';
-import { getTextExpansionError, TextExpansionCalloutLogic } from './text_expansion_callout_logic';
+import { TextExpansionCalloutLogic } from './text_expansion_callout_logic';
import { TextExpansionErrors } from './text_expansion_errors';
import { TRAINED_MODELS_PATH } from './utils';
@@ -78,24 +78,16 @@ export const TextExpansionCallOut: React.FC = (props)
const { dismiss, isCompact, isDismissable, show } = useTextExpansionCallOutData(props);
const { ingestionMethod } = useValues(IndexViewLogic);
const {
- createTextExpansionModelError,
- fetchTextExpansionModelError,
isCreateButtonDisabled,
isModelDownloadInProgress,
isModelDownloaded,
isModelRunningSingleThreaded,
isModelStarted,
+ textExpansionError,
isStartButtonDisabled,
- startTextExpansionModelError,
} = useValues(TextExpansionCalloutLogic);
- // In case of an error, show the error callout only
- const error = getTextExpansionError(
- createTextExpansionModelError,
- fetchTextExpansionModelError,
- startTextExpansionModelError
- );
- if (error) return ;
+ if (textExpansionError) return ;
if (!show) return null;
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout_logic.test.ts
index 744eb9af56042..0227f52b0c041 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout_logic.test.ts
@@ -13,6 +13,7 @@ import { ErrorResponse, HttpError, Status } from '../../../../../../../common/ty
import { MlModelDeploymentState } from '../../../../../../../common/types/ml';
import { CreateTextExpansionModelApiLogic } from '../../../../api/ml_models/text_expansion/create_text_expansion_model_api_logic';
import { FetchTextExpansionModelApiLogic } from '../../../../api/ml_models/text_expansion/fetch_text_expansion_model_api_logic';
+import { StartTextExpansionModelApiLogic } from '../../../../api/ml_models/text_expansion/start_text_expansion_model_api_logic';
import {
getTextExpansionError,
@@ -36,6 +37,7 @@ const DEFAULT_VALUES: TextExpansionCalloutValues = {
startTextExpansionModelStatus: Status.IDLE,
textExpansionModel: undefined,
textExpansionModelPollTimeoutId: null,
+ textExpansionError: null,
};
jest.useFakeTimers();
@@ -48,11 +50,15 @@ describe('TextExpansionCalloutLogic', () => {
const { mount: mountFetchTextExpansionModelApiLogic } = new LogicMounter(
FetchTextExpansionModelApiLogic
);
+ const { mount: mountStartTextExpansionModelApiLogic } = new LogicMounter(
+ StartTextExpansionModelApiLogic
+ );
beforeEach(() => {
jest.clearAllMocks();
mountCreateTextExpansionModelApiLogic();
mountFetchTextExpansionModelApiLogic();
+ mountStartTextExpansionModelApiLogic();
mount();
});
@@ -293,6 +299,44 @@ describe('TextExpansionCalloutLogic', () => {
});
});
+ describe('textExpansionError', () => {
+ const error = {
+ body: {
+ error: 'Error with ELSER deployment',
+ message: 'Mocked error message',
+ statusCode: 500,
+ },
+ } as HttpError;
+
+ it('returns null when there are no errors', () => {
+ CreateTextExpansionModelApiLogic.actions.apiReset();
+ FetchTextExpansionModelApiLogic.actions.apiReset();
+ StartTextExpansionModelApiLogic.actions.apiReset();
+ expect(TextExpansionCalloutLogic.values.textExpansionError).toBe(null);
+ });
+ it('returns extracted error for create', () => {
+ CreateTextExpansionModelApiLogic.actions.apiError(error);
+ expect(TextExpansionCalloutLogic.values.textExpansionError).toStrictEqual({
+ title: 'Error with ELSER deployment',
+ message: 'Mocked error message',
+ });
+ });
+ it('returns extracted error for fetch', () => {
+ FetchTextExpansionModelApiLogic.actions.apiError(error);
+ expect(TextExpansionCalloutLogic.values.textExpansionError).toStrictEqual({
+ title: 'Error fetching ELSER model',
+ message: 'Mocked error message',
+ });
+ });
+ it('returns extracted error for start', () => {
+ StartTextExpansionModelApiLogic.actions.apiError(error);
+ expect(TextExpansionCalloutLogic.values.textExpansionError).toStrictEqual({
+ title: 'Error starting ELSER deployment',
+ message: 'Mocked error message',
+ });
+ });
+ });
+
describe('isModelDownloadInProgress', () => {
it('is set to true if the model is downloading', () => {
FetchTextExpansionModelApiLogic.actions.apiSuccess({
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout_logic.ts
index a775ad3f05b89..86721c7808f14 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout_logic.ts
@@ -48,6 +48,11 @@ interface TextExpansionCalloutActions {
textExpansionModel: FetchTextExpansionModelApiLogicActions['apiSuccess'];
}
+export interface TextExpansionCalloutError {
+ title: string;
+ message: string;
+}
+
export interface TextExpansionCalloutValues {
createTextExpansionModelError: HttpError | undefined;
createTextExpansionModelStatus: Status;
@@ -64,6 +69,7 @@ export interface TextExpansionCalloutValues {
startTextExpansionModelStatus: Status;
textExpansionModel: FetchTextExpansionModelResponse | undefined;
textExpansionModelPollTimeoutId: null | ReturnType;
+ textExpansionError: TextExpansionCalloutError | null;
}
/**
@@ -257,6 +263,23 @@ export const TextExpansionCalloutLogic = kea<
(pollingTimeoutId: TextExpansionCalloutValues['textExpansionModelPollTimeoutId']) =>
pollingTimeoutId !== null,
],
+ textExpansionError: [
+ () => [
+ selectors.createTextExpansionModelError,
+ selectors.fetchTextExpansionModelError,
+ selectors.startTextExpansionModelError,
+ ],
+ (
+ createTextExpansionError: TextExpansionCalloutValues['createTextExpansionModelError'],
+ fetchTextExpansionError: TextExpansionCalloutValues['fetchTextExpansionModelError'],
+ startTextExpansionError: TextExpansionCalloutValues['startTextExpansionModelError']
+ ) =>
+ getTextExpansionError(
+ createTextExpansionError,
+ fetchTextExpansionError,
+ startTextExpansionError
+ ),
+ ],
isStartButtonDisabled: [
() => [selectors.startTextExpansionModelStatus],
(status: Status) => status !== Status.IDLE && status !== Status.ERROR,
From 34733bef94147c50e607fe734821865ad053888b Mon Sep 17 00:00:00 2001
From: GitStart <1501599+gitstart@users.noreply.github.com>
Date: Tue, 22 Aug 2023 14:16:32 +0100
Subject: [PATCH 08/26] [Watcher] Migrate all usages of EuiPage*_Deprecated
(#163128)
## What does this PR do?
* Migrate all usages of EuiPage*_Deprecated in Watcher
## Issue References
* https://github.com/elastic/kibana/issues/163070
## Video/Screenshot Demo
---
This code was written and reviewed by GitStart Community. Growing great
engineers, one PR at a time.
---------
Co-authored-by: LuisChiej <54555805+LuisChiej@users.noreply.github.com>
Co-authored-by: gitstart_bot
Co-authored-by: Yulia Cech
---
.../license_prompt.test.tsx.snap | 124 ++++++++----------
.../page_error/page_error_forbidden.tsx | 5 +-
.../page_error/page_error_not_exist.tsx | 5 +-
.../public/application/license_prompt.tsx | 31 +++--
.../json_watch_edit/json_watch_edit.tsx | 10 +-
.../monitoring_watch_edit.tsx | 30 ++---
.../threshold_watch_edit.tsx | 6 +-
.../watch_edit_page/watch_edit_page.tsx | 12 +-
.../watch_list_page/watch_list_page.tsx | 45 +++----
.../watch_status_page/watch_status_page.tsx | 12 +-
10 files changed, 125 insertions(+), 155 deletions(-)
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) {
From b5ef8a61c6c97a14b52a6088beca87b2bbb5fe70 Mon Sep 17 00:00:00 2001
From: Carlos Crespo
Date: Tue, 22 Aug 2023 15:31:56 +0200
Subject: [PATCH 09/26] [Infra UI] Add functional test for Node Details page
(#164174)
part of https://github.com/elastic/kibana/issues/162898
## Summary
This PR adds a functional test for the Node Details page using the Asset
Details component. It also refactors the Hosts View flyout test cases,
limiting them to high-level checks. The focus of Hosts View test focuses
on the page's own functionalities
Detailed Asset Details-related tests will be implemented in the Node
Details test suite.
### How to test
```bash
yarn test:ftr:server --config
x-pack/test/functional/apps/infra/config.ts
```
```bash
node scripts/functional_test_runner
--config=x-pack/test/functional/apps/infra/config.ts --include
x-pack/test/functional/apps/infra/hosts_view.ts
```
```bash
node scripts/functional_test_runner
--config=x-pack/test/functional/apps/infra/config.ts --include
x-pack/test/functional/apps/infra/node_details.ts
```
---
.../test/functional/apps/infra/constants.ts | 1 +
.../test/functional/apps/infra/hosts_view.ts | 165 ++------------
x-pack/test/functional/apps/infra/index.ts | 1 +
.../functional/apps/infra/node_details.ts | 215 ++++++++++++++++++
.../functional/page_objects/asset_details.ts | 132 +++++++++++
x-pack/test/functional/page_objects/index.ts | 2 +
.../page_objects/infra_hosts_view.ts | 113 ---------
7 files changed, 374 insertions(+), 255 deletions(-)
create mode 100644 x-pack/test/functional/apps/infra/node_details.ts
create mode 100644 x-pack/test/functional/page_objects/asset_details.ts
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/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');
From cd219942d12083897d7c56723f83da2a2f3c2586 Mon Sep 17 00:00:00 2001
From: James Gowdy
Date: Tue, 22 Aug 2023 14:39:58 +0100
Subject: [PATCH 10/26] [ML] Fix anomaly detection module manifest queries for
kibana sample data sets (#164332)
Follow up to https://github.com/elastic/kibana/pull/119635
All modules which contain a query in their manifest file should have a
filter to avoid querying cold and frozen tiers.
The original PR did not include the sample data sets as it was assumed
it was not necessary due to these data sets never being added to cold or
frozen tiers.
However it was overlooked that these queries will be run on any index
pattern passed to the `/internal/ml/modules/recognize` endpoint and so
has the potential to negatively impact the speed of all calls to this
endpoint, especially if the index pattern contains multiple indices and
wildcards.
---
.../modules/sample_data_ecommerce/manifest.json | 3 ++-
.../data_recognizer/modules/sample_data_weblogs/manifest.json | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
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": [
From 03e2ba9ad0e37bf7a7f1c3e985aa582b64d74c56 Mon Sep 17 00:00:00 2001
From: GitStart <1501599+gitstart@users.noreply.github.com>
Date: Tue, 22 Aug 2023 14:56:31 +0100
Subject: [PATCH 11/26] [Index Management] Migrate all usages of
EuiPage*_Deprecated (#163133)
---
.../component_template_create.tsx | 10 +--
.../component_template_edit.tsx | 10 +--
.../data_stream_list/data_stream_list.tsx | 6 +-
.../index_list/index_table/index_table.js | 71 ++++++++-----------
.../template_clone/template_clone.tsx | 6 +-
.../template_create/template_create.tsx | 6 +-
.../sections/template_edit/template_edit.tsx | 10 +--
7 files changed, 47 insertions(+), 72 deletions(-)
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 && (
-
+
);
};
From 813eebe1cef0fa63245f1a9df2c0e1b5a1945b6c Mon Sep 17 00:00:00 2001
From: Pablo Machado
Date: Tue, 22 Aug 2023 16:02:11 +0200
Subject: [PATCH 12/26] [Security Solutions] Add telemetry to the side
navigation tracking for serverless pages (#164309)
issue: https://github.com/elastic/kibana/issues/164306
## Summary
Add telemetry to serverless-specific Security
pages
Pages: Investigations, Assets, ML, Project Settings.
## How to test it
* Visit Investigations, Assets, ML, and Project Settings.
When you navigate between pages the app should log "Reporting
application usage for securitySolutionUI, {PAGE}" like it does for the
other pages
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../security_solution_serverless/kibana.jsonc | 5 ++-
.../public/pages/assets.tsx | 37 ++++++++++---------
.../public/pages/investigations.tsx | 11 ++++--
.../public/pages/machine_learning.tsx | 11 ++++--
.../public/pages/project_settings.tsx | 17 +++++----
.../tsconfig.json | 3 +-
6 files changed, 50 insertions(+), 34 deletions(-)
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"
]
}
From 9c83d2eeb160ca0434127b9b1ddbe1432836e9ce Mon Sep 17 00:00:00 2001
From: Alison Goryachev
Date: Tue, 22 Aug 2023 10:29:42 -0400
Subject: [PATCH 13/26] [Ingest Pipelines] Add serverless test coverage
(#163208)
---
.../ingest_pipelines/ingest_pipelines.ts | 344 ++++----------
.../management/ingest_pipelines/lib/api.ts | 69 +++
.../ingest_pipelines/lib/elasticsearch.ts | 70 ---
.../ingest_pipelines/lib/fixtures.ts | 109 +++++
.../management/ingest_pipelines/lib/index.ts | 3 +-
x-pack/test/api_integration/services/index.ts | 2 +
.../services/ingest_pipelines.ts | 22 +
.../test_suites/common/index.ts | 1 +
.../test_suites/common/ingest_pipelines.ts | 440 ++++++++++++++++++
9 files changed, 744 insertions(+), 316 deletions(-)
create mode 100644 x-pack/test/api_integration/apis/management/ingest_pipelines/lib/api.ts
delete mode 100644 x-pack/test/api_integration/apis/management/ingest_pipelines/lib/elasticsearch.ts
create mode 100644 x-pack/test/api_integration/apis/management/ingest_pipelines/lib/fixtures.ts
create mode 100644 x-pack/test/api_integration/services/ingest_pipelines.ts
create mode 100644 x-pack/test_serverless/api_integration/test_suites/common/ingest_pipelines.ts
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/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_serverless/api_integration/test_suites/common/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/index.ts
index 1bfb13f2c5f2c..dbd561a07197c 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
@@ -15,5 +15,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',
+ },
+ },
+ ]);
+ });
+ });
+ });
+}
From c2a552ef66ac8e0c2a0372091ba85b3bf1fa7a3a Mon Sep 17 00:00:00 2001
From: Konrad Szwarc
Date: Tue, 22 Aug 2023 16:33:39 +0200
Subject: [PATCH 14/26] [Fleet][Kafka] When compression is enabled, "None"
shouldn't be an option for codec (#164416)
Closes https://github.com/elastic/ingest-dev/issues/2327
`None` option removed from UI, tests aligned.
https://github.com/elastic/kibana/assets/29123534/b8b7ac7a-e3e9-4800-94b9-f80e1f42f044
---
.../fleet/cypress/e2e/fleet_settings_outputs.cy.ts | 2 --
.../output_form_kafka_compression.tsx | 8 ++++----
.../edit_output_flyout/use_output_form.tsx | 12 +++++++++---
.../services/agent_policies/full_agent_policy.ts | 4 ++--
x-pack/plugins/fleet/server/services/output.ts | 5 +++++
5 files changed, 20 insertions(+), 11 deletions(-)
diff --git a/x-pack/plugins/fleet/cypress/e2e/fleet_settings_outputs.cy.ts b/x-pack/plugins/fleet/cypress/e2e/fleet_settings_outputs.cy.ts
index 2d9f33d8e1efa..c5cf05bc1edd3 100644
--- a/x-pack/plugins/fleet/cypress/e2e/fleet_settings_outputs.cy.ts
+++ b/x-pack/plugins/fleet/cypress/e2e/fleet_settings_outputs.cy.ts
@@ -184,8 +184,6 @@ describe('Outputs', () => {
// Compression
cy.getBySel(SETTINGS_OUTPUTS_KAFKA.COMPRESSION_CODEC_INPUT).should('not.exist');
cy.getBySel(SETTINGS_OUTPUTS_KAFKA.COMPRESSION_SWITCH).click();
- cy.getBySel(SETTINGS_OUTPUTS_KAFKA.COMPRESSION_LEVEL_INPUT).should('not.exist');
- cy.getBySel(SETTINGS_OUTPUTS_KAFKA.COMPRESSION_CODEC_INPUT).select('gzip');
cy.getBySel(SETTINGS_OUTPUTS_KAFKA.COMPRESSION_LEVEL_INPUT).should('exist');
cy.getBySel(SETTINGS_OUTPUTS_KAFKA.COMPRESSION_LEVEL_INPUT).select('1');
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka_compression.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka_compression.tsx
index 704fdfa893603..b11ef116920b8 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka_compression.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_kafka_compression.tsx
@@ -21,12 +21,12 @@ export const OutputFormKafkaCompression: React.FunctionComponent<{
const kafkaCompressionTypeOptions = useMemo(
() =>
- (Object.keys(kafkaCompressionType) as Array).map(
- (key) => ({
+ (Object.keys(kafkaCompressionType) as Array)
+ .filter((c) => c !== 'None')
+ .map((key) => ({
text: kafkaCompressionType[key],
label: kafkaCompressionType[key],
- })
- ),
+ })),
[]
);
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx
index bd1891f3ebc12..6c22ec05b6ece 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx
@@ -391,7 +391,9 @@ export function useOutputForm(onSucess: () => void, output?: Output) {
isDisabled('compression_level')
);
const kafkaCompressionCodecInput = useInput(
- kafkaOutput?.compression ?? kafkaCompressionType.None,
+ kafkaOutput?.compression && kafkaOutput.compression !== kafkaCompressionType.None
+ ? kafkaOutput.compression
+ : kafkaCompressionType.Gzip,
undefined,
isDisabled('compression')
);
@@ -642,8 +644,11 @@ export function useOutputForm(onSucess: () => void, output?: Output) {
client_id: kafkaClientIdInput.value || undefined,
version: kafkaVersionInput.value,
...(kafkaKeyInput.value ? { key: kafkaKeyInput.value } : {}),
- compression: kafkaCompressionCodecInput.value,
- ...(kafkaCompressionCodecInput.value === kafkaCompressionType.Gzip
+ compression: kafkaCompressionInput.value
+ ? kafkaCompressionCodecInput.value
+ : kafkaCompressionType.None,
+ ...(kafkaCompressionInput.value &&
+ kafkaCompressionCodecInput.value === kafkaCompressionType.Gzip
? {
compression_level: parseIntegerIfStringDefined(
kafkaCompressionLevelInput.value
@@ -785,6 +790,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) {
loadBalanceEnabledInput.value,
typeInput.value,
kafkaSslCertificateAuthoritiesInput.value,
+ kafkaCompressionInput.value,
nameInput.value,
kafkaHostsInput.value,
defaultOutputInput.value,
diff --git a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts
index 0a05d178ac745..9107a334dad83 100644
--- a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts
@@ -20,7 +20,7 @@ import type {
} from '../../types';
import type { FullAgentPolicyOutputPermissions, PackageInfo } from '../../../common/types';
import { agentPolicyService } from '../agent_policy';
-import { dataTypes, outputType } from '../../../common/constants';
+import { dataTypes, kafkaCompressionType, outputType } from '../../../common/constants';
import { DEFAULT_OUTPUT } from '../../constants';
import { getPackageInfo } from '../epm/packages';
@@ -344,7 +344,7 @@ export function transformOutputToFullPolicyOutput(
version,
key,
compression,
- compression_level,
+ ...(compression === kafkaCompressionType.Gzip ? { compression_level } : {}),
...(username ? { username } : {}),
...(password ? { password } : {}),
...(sasl ? { sasl } : {}),
diff --git a/x-pack/plugins/fleet/server/services/output.ts b/x-pack/plugins/fleet/server/services/output.ts
index f190fc7d2315f..15b256fc84207 100644
--- a/x-pack/plugins/fleet/server/services/output.ts
+++ b/x-pack/plugins/fleet/server/services/output.ts
@@ -764,6 +764,11 @@ class OutputService {
) {
updateData.compression_level = 4;
}
+ if (data.compression && data.compression !== kafkaCompressionType.Gzip) {
+ // Clear compression level if compression is not gzip
+ updateData.compression_level = null;
+ }
+
if (!data.client_id) {
updateData.client_id = 'Elastic';
}
From 209e7750d8df31e634efa1b0bf38000728aafc6c Mon Sep 17 00:00:00 2001
From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com>
Date: Tue, 22 Aug 2023 10:56:15 -0400
Subject: [PATCH 15/26] [Security Solution] [Timeline] Open a timeline to any
tab from a url without a saved object id (#163033)
## Summary
This change enables the timeline to be opened to any specific tab on
page load without needing a saved object id, normally generated when a
user creates some sort of state that is either auto saved or explicitly
saved by a user action. The video below is just a hard coded
window.location = security url redirect in the discover plugin, the
discover part will come in a follow up pr.
![open_timeline_discover_tab](https://github.com/elastic/kibana/assets/56408403/ef834d72-c42e-4584-bf98-1d8ef29dd530)
### Checklist
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../public/app/home/index.test.tsx | 8 +-
.../timeline/use_init_timeline_url_param.ts | 2 +-
.../timeline/use_sync_timeline_url_param.ts | 17 ++-
.../common/hooks/use_resolve_conflict.tsx | 15 +--
.../components/open_timeline/helpers.ts | 116 ++++++++++--------
.../timelines/components/timeline/index.tsx | 4 +-
.../timeline/query_tab_content/index.tsx | 2 +-
.../timelines/store/timeline/helpers.ts | 2 +-
.../public/timelines/store/timeline/model.ts | 2 +-
.../investigations/timelines/url_state.cy.ts | 69 +++++++++++
.../cypress/e2e/urls/state.cy.ts | 56 +++++----
11 files changed, 194 insertions(+), 99 deletions(-)
create mode 100644 x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/url_state.cy.ts
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/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/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')))`
);
});
From c90e6d11c2ecb2010ed54b0f8cb37ac423778c4b Mon Sep 17 00:00:00 2001
From: Abdul Wahab Zahid
Date: Tue, 22 Aug 2023 17:18:05 +0200
Subject: [PATCH 16/26] [Synthetics] Fix flay test`EnableDefaultAlerting`
alert. (#164291)
Resolves #158408.
---
.../apis/synthetics/enable_default_alerting.ts | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
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',
},
};
From 7c896218dd4dc37bc8ef27fdcd9ee7056b65811c Mon Sep 17 00:00:00 2001
From: Cee Chen <549407+cee-chen@users.noreply.github.com>
Date: Tue, 22 Aug 2023 08:33:37 -0700
Subject: [PATCH 17/26] Remove several `@ts-ignore`/`@ts-expect-error`s around
EUI imports (#163984)
## Summary
There's a few EUI imports out there that folks are reaching into
`@elastic/eui/lib/` for (which doesn't contain any types - something I'm
looking into separately) that could instead be imported at the top
`@elastic/eui` level, which is properly typed.
### Checklist
N/A - types only
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../components/code_editor/code_editor.test.tsx | 3 +--
.../visualizations/point_series/_point_series.js | 2 +-
src/plugins/vis_types/xy/public/vis_types/area.ts | 3 +--
.../vis_types/xy/public/vis_types/histogram.ts | 3 +--
.../xy/public/vis_types/horizontal_bar.ts | 3 +--
src/plugins/vis_types/xy/public/vis_types/line.ts | 3 +--
.../src/document_count_chart/brush_badge.tsx | 3 +--
.../components/all_cases/use_cases_columns.tsx | 2 +-
.../field_data_expanded_row/date_content.tsx | 3 +--
.../plugins/graph/public/helpers/style_choices.ts | 3 +--
.../log_entry_rate/sections/anomalies/table.tsx | 2 +-
.../maps/public/classes/styles/color_palettes.ts | 13 ++-----------
.../vector/components/color/color_stops_utils.js | 3 +--
.../extract_color_from_style_property.test.ts | 3 +--
.../annotations_table/annotations_table.js | 3 +--
.../job_selector_table/job_selector_table.js | 4 +++-
.../configuration_step/analysis_fields_table.tsx | 14 ++++++++++----
.../job_details/forecasts_table/forecasts_table.js | 2 +-
.../public/components/chart/get_color.js | 2 +-
.../server/authentication/unauthenticated_page.tsx | 3 +--
.../server/authorization/reset_session_page.tsx | 3 +--
.../restore_list/restore_table/restore_table.tsx | 11 ++++++++---
.../simple/ping_list/expanded_row.tsx | 3 +--
.../rule_details/components/rule_alert_list.tsx | 3 +--
.../components/monitor/ping_list/expanded_row.tsx | 3 +--
25 files changed, 44 insertions(+), 56 deletions(-)
diff --git a/src/plugins/es_ui_shared/public/components/code_editor/code_editor.test.tsx b/src/plugins/es_ui_shared/public/components/code_editor/code_editor.test.tsx
index b5e23bfc3f95b..9092d94936ffe 100644
--- a/src/plugins/es_ui_shared/public/components/code_editor/code_editor.test.tsx
+++ b/src/plugins/es_ui_shared/public/components/code_editor/code_editor.test.tsx
@@ -9,8 +9,7 @@
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import EuiCodeEditor from './code_editor';
-// @ts-ignore
-import { keys } from '@elastic/eui/lib/services';
+import { keys } from '@elastic/eui';
import { findTestSubject, requiredProps, takeMountedSnapshot } from '@elastic/eui/lib/test';
describe('EuiCodeEditor', () => {
diff --git a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/_point_series.js b/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/_point_series.js
index dddfde75dbb82..4756c9bf32704 100644
--- a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/_point_series.js
+++ b/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/_point_series.js
@@ -7,7 +7,7 @@
*/
import _ from 'lodash';
-import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
+import { euiPaletteColorBlind } from '@elastic/eui';
const thresholdLineDefaults = {
show: false,
diff --git a/src/plugins/vis_types/xy/public/vis_types/area.ts b/src/plugins/vis_types/xy/public/vis_types/area.ts
index 34afc54720e9b..610d6280343c5 100644
--- a/src/plugins/vis_types/xy/public/vis_types/area.ts
+++ b/src/plugins/vis_types/xy/public/vis_types/area.ts
@@ -7,8 +7,7 @@
*/
import { i18n } from '@kbn/i18n';
-// @ts-ignore
-import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
+import { euiPaletteColorBlind } from '@elastic/eui';
import { Fit, Position } from '@elastic/charts';
import { AggGroupNames } from '@kbn/data-plugin/public';
diff --git a/src/plugins/vis_types/xy/public/vis_types/histogram.ts b/src/plugins/vis_types/xy/public/vis_types/histogram.ts
index 0a898f62ef227..df910de2f3808 100644
--- a/src/plugins/vis_types/xy/public/vis_types/histogram.ts
+++ b/src/plugins/vis_types/xy/public/vis_types/histogram.ts
@@ -7,8 +7,7 @@
*/
import { i18n } from '@kbn/i18n';
-// @ts-ignore
-import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
+import { euiPaletteColorBlind } from '@elastic/eui';
import { Position } from '@elastic/charts';
import { AggGroupNames } from '@kbn/data-plugin/public';
diff --git a/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts b/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts
index dfbf51aa6d684..ba578ab916e81 100644
--- a/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts
+++ b/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts
@@ -7,8 +7,7 @@
*/
import { i18n } from '@kbn/i18n';
-// @ts-ignore
-import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
+import { euiPaletteColorBlind } from '@elastic/eui';
import { Position } from '@elastic/charts';
import { AggGroupNames } from '@kbn/data-plugin/public';
diff --git a/src/plugins/vis_types/xy/public/vis_types/line.ts b/src/plugins/vis_types/xy/public/vis_types/line.ts
index 49113e2a21b27..272d38e982045 100644
--- a/src/plugins/vis_types/xy/public/vis_types/line.ts
+++ b/src/plugins/vis_types/xy/public/vis_types/line.ts
@@ -7,8 +7,7 @@
*/
import { i18n } from '@kbn/i18n';
-// @ts-ignore
-import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
+import { euiPaletteColorBlind } from '@elastic/eui';
import { Position, Fit } from '@elastic/charts';
import { AggGroupNames } from '@kbn/data-plugin/public';
diff --git a/x-pack/packages/ml/aiops_components/src/document_count_chart/brush_badge.tsx b/x-pack/packages/ml/aiops_components/src/document_count_chart/brush_badge.tsx
index 53563a721026b..15bccea10c8a1 100644
--- a/x-pack/packages/ml/aiops_components/src/document_count_chart/brush_badge.tsx
+++ b/x-pack/packages/ml/aiops_components/src/document_count_chart/brush_badge.tsx
@@ -8,8 +8,7 @@
import React, { FC } from 'react';
import { EuiBadge, EuiText, EuiToolTip } from '@elastic/eui';
-// @ts-ignore
-import { formatDate } from '@elastic/eui/lib/services/format';
+import { formatDate } from '@elastic/eui';
const DATE_FORMAT = 'YYYY-MM-DD';
const TIME_FORMAT = 'HH:mm:ss';
diff --git a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.tsx b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.tsx
index ffcc58b531693..0863e51daef5c 100644
--- a/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/use_cases_columns.tsx
@@ -19,8 +19,8 @@ import {
EuiIcon,
EuiHealth,
EuiToolTip,
+ RIGHT_ALIGNMENT,
} from '@elastic/eui';
-import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services';
import styled from 'styled-components';
import { Status } from '@kbn/cases-components/src/status/status';
import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/date_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/date_content.tsx
index 98370dabb1f0a..6345412e1651e 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/date_content.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/date_content.tsx
@@ -7,8 +7,7 @@
import React, { FC, ReactNode } from 'react';
import { EuiBasicTable, HorizontalAlignment, LEFT_ALIGNMENT, RIGHT_ALIGNMENT } from '@elastic/eui';
-// @ts-ignore
-import { formatDate } from '@elastic/eui/lib/services/format';
+import { formatDate } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
diff --git a/x-pack/plugins/graph/public/helpers/style_choices.ts b/x-pack/plugins/graph/public/helpers/style_choices.ts
index 0030c91c5aadf..b64abb27fbe25 100644
--- a/x-pack/plugins/graph/public/helpers/style_choices.ts
+++ b/x-pack/plugins/graph/public/helpers/style_choices.ts
@@ -6,8 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
-// @ts-ignore
-import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
+import { euiPaletteColorBlind } from '@elastic/eui';
export interface GenericIcon {
label: string;
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/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/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/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,
From deb64c19cf63c054ca15ba3ec8217e031dd37173 Mon Sep 17 00:00:00 2001
From: Tim Sullivan
Date: Tue, 22 Aug 2023 09:20:40 -0700
Subject: [PATCH 18/26] Reporting/fix visual warning test (#164383)
## Summary
Closes https://github.com/elastic/kibana/issues/135309
This PR eliminates a skipped functional test by replacing the test
coverage with unit tests.
*
`x-pack/plugins/screenshotting/server/screenshots/screenshots.test.ts`:
ensures that waiting too long for the URL to open will return the
expected error message
*
`x-pack/plugins/screenshotting/server/browsers/chromium/driver.test.ts`:
ensures that when the screenshot capture method is passed an error
message, that error message is injected into the screenshot
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.buildkite/ftr_configs.yml | 1 -
.../server/__mocks__/puppeteer.ts | 47 ++++
.../server/browsers/chromium/driver.test.ts | 107 +++++++++
.../chromium/driver_factory/index.test.ts | 36 ++-
.../server/screenshots/index.ts | 201 +----------------
.../server/screenshots/screenshots.test.ts | 166 ++++++++++++++
.../server/screenshots/screenshots.ts | 208 ++++++++++++++++++
x-pack/plugins/screenshotting/tsconfig.json | 2 +
.../reporting_and_timeout.config.ts | 26 ---
.../fixtures/baseline/warnings_capture_b.png | Bin 95272 -> 0 bytes
.../reporting_and_timeout/index.ts | 98 ---------
11 files changed, 549 insertions(+), 343 deletions(-)
create mode 100644 x-pack/plugins/screenshotting/server/__mocks__/puppeteer.ts
create mode 100644 x-pack/plugins/screenshotting/server/browsers/chromium/driver.test.ts
create mode 100644 x-pack/plugins/screenshotting/server/screenshots/screenshots.test.ts
create mode 100644 x-pack/plugins/screenshotting/server/screenshots/screenshots.ts
delete mode 100644 x-pack/test/reporting_functional/reporting_and_timeout.config.ts
delete mode 100644 x-pack/test/reporting_functional/reporting_and_timeout/fixtures/baseline/warnings_capture_b.png
delete mode 100644 x-pack/test/reporting_functional/reporting_and_timeout/index.ts
diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml
index b26b54b767658..8f1546099d539 100644
--- a/.buildkite/ftr_configs.yml
+++ b/.buildkite/ftr_configs.yml
@@ -353,7 +353,6 @@ enabled:
- x-pack/test/reporting_functional/reporting_and_deprecated_security.config.ts
- x-pack/test/reporting_functional/reporting_and_security.config.ts
- x-pack/test/reporting_functional/reporting_without_security.config.ts
- - x-pack/test/reporting_functional/reporting_and_timeout.config.ts
- x-pack/test/rule_registry/security_and_spaces/config_basic.ts
- x-pack/test/rule_registry/security_and_spaces/config_trial.ts
- x-pack/test/rule_registry/spaces_only/config_basic.ts
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/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 e86939f7ff741ef93185ebefa86052b49c908f85..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 95272
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
t-%8!
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