From fd1c63a2c7dfa8cbd368125df0ed6bb38e18e2e5 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Mon, 7 Feb 2022 14:53:19 -0500 Subject: [PATCH 01/44] [DOCS] Fix typo (#124872) Fixes a minor typo in the 8.x upgrade docs. --- docs/setup/upgrade.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/setup/upgrade.asciidoc b/docs/setup/upgrade.asciidoc index d8e08b460e5f6..4eabfa0c07714 100644 --- a/docs/setup/upgrade.asciidoc +++ b/docs/setup/upgrade.asciidoc @@ -2,7 +2,7 @@ == Upgrade {kib} To upgrade from 7.16 or earlier to {version}, -**You must first upgrade to {prev-major-last}**. +**you must first upgrade to {prev-major-last}**. This enables you to use the Upgrade Assistant to {stack-ref}/upgrading-elastic-stack.html#prepare-to-upgrade[prepare to upgrade]. You must resolve all critical issues identified by the Upgrade Assistant From 91b0b5f0261688265c44fcb646f201ad70403d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Mon, 7 Feb 2022 15:03:53 -0500 Subject: [PATCH 02/44] Initial commit (#124858) --- x-pack/plugins/actions/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index f838832b6ea66..3c5c459d5a780 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -556,6 +556,8 @@ When creating a new action type, your plugin will eventually call `server.plugin Consider working with the alerting team on early structure /design feedback of new actions, especially as the APIs and infrastructure are still under development. +Don't forget to ping @elastic/security-detections-response to see if the new connector should be enabled within their solution. + ## licensing Currently actions are licensed as "basic" if the action only interacts with the stack, eg the server log and es index actions. Other actions are at least "gold" level. From aedbc9f4c95e053878c1e4628d75b95ee2ff00ab Mon Sep 17 00:00:00 2001 From: Madison Caldwell Date: Mon, 7 Feb 2022 16:08:09 -0500 Subject: [PATCH 03/44] Filter out 'signal.*' fields to prevent alias clashes (#124471) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../factories/utils/filter_source.ts | 2 + .../tests/generating_signals.ts | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/filter_source.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/filter_source.ts index 3493025749f98..35c91ba398f6f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/filter_source.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/filter_source.ts @@ -13,11 +13,13 @@ export const filterSource = (doc: SignalSourceHit): Partial => { const docSource = doc._source ?? {}; const { event, + signal, threshold_result: siemSignalsThresholdResult, [ALERT_THRESHOLD_RESULT]: alertThresholdResult, ...filteredSource } = docSource || { event: null, + signal: null, threshold_result: null, [ALERT_THRESHOLD_RESULT]: null, }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index efdf862c3070e..6dd569d891fdc 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -941,6 +941,49 @@ export default ({ getService }: FtrProviderContext) => { }); }); + /** + * Here we test that 8.0.x alerts can be generated on legacy (pre-8.x) alerts. + */ + describe('Signals generated from legacy signals', async () => { + beforeEach(async () => { + await deleteSignalsIndex(supertest, log); + await createSignalsIndex(supertest, log); + await esArchiver.load( + 'x-pack/test/functional/es_archives/security_solution/legacy_cti_signals' + ); + }); + + afterEach(async () => { + await esArchiver.unload( + 'x-pack/test/functional/es_archives/security_solution/legacy_cti_signals' + ); + await deleteSignalsIndex(supertest, log); + await deleteAllAlerts(supertest, log); + }); + + it('should generate a signal-on-legacy-signal with legacy index pattern', async () => { + const rule: QueryCreateSchema = { + ...getRuleForSignalTesting([`.siem-signals-*`]), + }; + const { id } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 1, [id]); + const signalsOpen = await getSignalsByIds(supertest, log, [id]); + expect(signalsOpen.hits.hits.length).greaterThan(0); + }); + + it('should generate a signal-on-legacy-signal with AAD index pattern', async () => { + const rule: QueryCreateSchema = { + ...getRuleForSignalTesting([`.alerts-security.alerts-default`]), + }; + const { id } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 1, [id]); + const signalsOpen = await getSignalsByIds(supertest, log, [id]); + expect(signalsOpen.hits.hits.length).greaterThan(0); + }); + }); + /** * Here we test the functionality of Severity and Risk Score overrides (also called "mappings" * in the code). If the rule specifies a mapping, then the final Severity or Risk Score From 270adf49587db4f77dbe3c46f41f92e5d739b6c1 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Mon, 7 Feb 2022 16:38:24 -0500 Subject: [PATCH 04/44] [Alerting] Rename alert instance to alert and changing signature of alert (instance) factory alert creation (#124390) * Rename alert instance to alert and add create fn to alert factory * Rename alert instance to alert and add create fn to alert factory * Fixing types * Fixing types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/alert_types/always_firing.ts | 4 +- .../server/alert_types/astros.ts | 2 +- x-pack/plugins/alerting/README.md | 15 +- .../alerting/server/alert/alert.test.ts | 488 ++++++++++++++ .../alert_instance.ts => alert/alert.ts} | 6 +- .../create_alert_factory.test.ts} | 32 +- .../server/alert/create_alert_factory.ts | 33 + .../server/{alert_instance => alert}/index.ts | 6 +- .../alert_instance/alert_instance.test.ts | 604 ------------------ .../create_alert_instance_factory.ts | 23 - x-pack/plugins/alerting/server/index.ts | 2 +- x-pack/plugins/alerting/server/mocks.ts | 59 +- .../server/rules_client/rules_client.ts | 106 +-- .../server/task_runner/task_runner.test.ts | 46 +- .../server/task_runner/task_runner.ts | 44 +- .../task_runner/task_runner_cancel.test.ts | 6 +- x-pack/plugins/alerting/server/types.ts | 8 +- .../register_error_count_alert_type.test.ts | 4 +- ...action_duration_anomaly_alert_type.test.ts | 10 +- ..._transaction_error_rate_alert_type.test.ts | 8 +- .../server/routes/alerts/test_utils/index.ts | 2 +- .../inventory_metric_threshold_executor.ts | 5 +- .../log_threshold_executor.test.ts | 4 +- .../log_threshold/log_threshold_executor.ts | 2 +- .../metric_anomaly/metric_anomaly_executor.ts | 2 +- .../metric_threshold_executor.test.ts | 4 +- .../metric_threshold_executor.ts | 5 +- .../register_anomaly_detection_alert_type.ts | 2 +- .../register_jobs_monitoring_rule_type.ts | 2 +- .../monitoring/server/alerts/base_rule.ts | 15 +- .../alerts/ccr_read_exceptions_rule.test.ts | 16 +- .../server/alerts/ccr_read_exceptions_rule.ts | 4 +- .../server/alerts/cluster_health_rule.test.ts | 16 +- .../server/alerts/cluster_health_rule.ts | 4 +- .../server/alerts/cpu_usage_rule.test.ts | 16 +- .../server/alerts/cpu_usage_rule.ts | 4 +- .../server/alerts/disk_usage_rule.test.ts | 16 +- .../server/alerts/disk_usage_rule.ts | 4 +- ...lasticsearch_version_mismatch_rule.test.ts | 16 +- .../elasticsearch_version_mismatch_rule.ts | 4 +- .../kibana_version_mismatch_rule.test.ts | 16 +- .../alerts/kibana_version_mismatch_rule.ts | 4 +- .../alerts/large_shard_size_rule.test.ts | 16 +- .../server/alerts/large_shard_size_rule.ts | 4 +- .../alerts/license_expiration_rule.test.ts | 16 +- .../server/alerts/license_expiration_rule.ts | 4 +- .../logstash_version_mismatch_rule.test.ts | 16 +- .../alerts/logstash_version_mismatch_rule.ts | 4 +- .../server/alerts/memory_usage_rule.test.ts | 16 +- .../server/alerts/memory_usage_rule.ts | 4 +- .../missing_monitoring_data_rule.test.ts | 16 +- .../alerts/missing_monitoring_data_rule.ts | 4 +- .../server/alerts/nodes_changed_rule.test.ts | 16 +- .../server/alerts/nodes_changed_rule.ts | 4 +- .../thread_pool_rejections_rule_base.ts | 8 +- ...thread_pool_search_rejections_rule.test.ts | 16 +- .../thread_pool_write_rejections_rule.test.ts | 16 +- .../server/utils/create_lifecycle_executor.ts | 8 +- .../utils/create_lifecycle_rule_type.test.ts | 12 +- .../utils/lifecycle_alert_services_mock.ts | 2 +- .../server/utils/rule_executor_test_utils.ts | 3 +- ...gacy_rules_notification_alert_type.test.ts | 20 +- .../legacy_rules_notification_alert_type.ts | 2 +- .../schedule_notification_actions.test.ts | 2 +- .../schedule_notification_actions.ts | 6 +- ...dule_throttle_notification_actions.test.ts | 24 +- .../schedule_throttle_notification_actions.ts | 4 +- .../routes/rules/preview_rules_route.ts | 28 +- .../rule_types/__mocks__/rule_type.ts | 2 +- .../create_security_rule_type_wrapper.ts | 8 +- .../preview/alert_instance_factory_stub.ts | 8 +- .../alert_types/es_query/alert_type.test.ts | 18 +- .../server/alert_types/es_query/alert_type.ts | 4 +- .../geo_containment/geo_containment.ts | 8 +- .../tests/geo_containment.test.ts | 29 +- .../index_threshold/alert_type.test.ts | 6 +- .../alert_types/index_threshold/alert_type.ts | 4 +- .../register_transform_health_rule_type.ts | 4 +- .../uptime/server/lib/alerts/tls_legacy.ts | 11 +- .../plugins/alerts/server/alert_types.ts | 14 +- .../fixtures/plugins/alerts/server/plugin.ts | 4 +- .../tests/trial/lifecycle_executor.ts | 2 +- 82 files changed, 997 insertions(+), 1065 deletions(-) create mode 100644 x-pack/plugins/alerting/server/alert/alert.test.ts rename x-pack/plugins/alerting/server/{alert_instance/alert_instance.ts => alert/alert.ts} (97%) rename x-pack/plugins/alerting/server/{alert_instance/create_alert_instance_factory.test.ts => alert/create_alert_factory.test.ts} (61%) create mode 100644 x-pack/plugins/alerting/server/alert/create_alert_factory.ts rename x-pack/plugins/alerting/server/{alert_instance => alert}/index.ts (57%) delete mode 100644 x-pack/plugins/alerting/server/alert_instance/alert_instance.test.ts delete mode 100644 x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.ts diff --git a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts index dc89a473a38ab..1896db426da5f 100644 --- a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts +++ b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts @@ -65,8 +65,8 @@ export const alertType: RuleType< range(instances) .map(() => uuid.v4()) .forEach((id: string) => { - services - .alertInstanceFactory(id) + services.alertFactory + .create(id) .replaceState({ triggerdOnCycle: count }) .scheduleActions(getTShirtSizeByIdAndThreshold(id, thresholds)); }); diff --git a/x-pack/examples/alerting_example/server/alert_types/astros.ts b/x-pack/examples/alerting_example/server/alert_types/astros.ts index c5d4af6872c83..a29b280a34fff 100644 --- a/x-pack/examples/alerting_example/server/alert_types/astros.ts +++ b/x-pack/examples/alerting_example/server/alert_types/astros.ts @@ -70,7 +70,7 @@ export const alertType: RuleType< if (getOperator(op)(peopleInCraft.length, outerSpaceCapacity)) { peopleInCraft.forEach(({ craft, name }) => { - services.alertInstanceFactory(name).replaceState({ craft }).scheduleActions('default'); + services.alertFactory.create(name).replaceState({ craft }).scheduleActions('default'); }); } diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md index 90c02b6e1c254..bc917fbf43bc4 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerting/README.md @@ -40,8 +40,6 @@ Table of Contents > References to `rule` and `rule type` entities are still named `AlertType` within the codebase. -> References to `alert` and `alert factory` entities are still named `AlertInstance` and `alertInstanceFactory` within the codebase. - **Rule Type**: A function that takes parameters and executes actions on alerts. **Rule**: A configuration that defines a schedule, a rule type w/ parameters, state information and actions. @@ -113,7 +111,7 @@ This is the primary function for a rule type. Whenever the rule needs to execute |---|---| |services.scopedClusterClient|This is an instance of the Elasticsearch client. Use this to do Elasticsearch queries in the context of the user who created the alert when security is enabled.| |services.savedObjectsClient|This is an instance of the saved objects client. This provides the ability to perform CRUD operations on any saved object that lives in the same space as the rule.

The scope of the saved objects client is tied to the user who created the rule (only when security is enabled).| -|services.alertInstanceFactory(id)|This [alert factory](#alert-factory) creates alerts and must be used in order to execute actions. The id you give to the alert factory is a unique identifier for the alert.| +|services.alertFactory|This [alert factory](#alert-factory) creates alerts and must be used in order to schedule action execution. The id you give to the alert factory create function() is a unique identifier for the alert.| |services.log(tags, [data], [timestamp])|Use this to create server logs. (This is the same function as server.log)| |services.shouldWriteAlerts()|This returns a boolean indicating whether the executor should write out alerts as data. This is determined by whether rule execution has been cancelled due to timeout AND whether both the Kibana `cancelAlertsOnRuleTimeout` flag and the rule type `cancelAlertsOnRuleTimeout` are set to `true`.| |services.shouldStopExecution()|This returns a boolean indicating whether rule execution has been cancelled due to timeout.| @@ -310,7 +308,7 @@ const myRuleType: RuleType< // scenario the provided server will be used. Also, this ID will be // used to make `getState()` return previous state, if any, on // matching identifiers. - const alert = services.alertInstanceFactory(server); + const alert = services.alertFactory.create(server); // State from the last execution. This will exist if an alert was // created and executed in the previous execution @@ -731,13 +729,13 @@ Query: ## Alert Factory -**alertInstanceFactory(id)** +**alertFactory.create(id)** -One service passed in to each rule type is the alert factory. This factory creates alerts and must be used in order to execute actions. The `id` you give to the alert factory is the unique identifier for the alert (e.g. the server identifier if the alert is about servers). The alert factory will use this identifier to retrieve the state of previous alerts with the same `id`. These alerts support persisting state between rule executions, but will clear out once the alert stops firing. +One service passed in to each rule type is the alert factory. This factory creates alerts and must be used in order to schedule action execution. The `id` you give to the alert factory create fn() is the unique identifier for the alert (e.g. the server identifier if the alert is about servers). The alert factory will use this identifier to retrieve the state of previous alerts with the same `id`. These alerts support persisting state between rule executions, but will clear out once the alert stops firing. Note that the `id` only needs to be unique **within the scope of a specific rule**, not unique across all rules or rule types. For example, Rule 1 and Rule 2 can both create an alert with an `id` of `"a"` without conflicting with one another. But if Rule 1 creates 2 alerts, then they must be differentiated with `id`s of `"a"` and `"b"`. -This factory returns an instance of `AlertInstance`. The `AlertInstance` class has the following methods. Note that we have removed the methods that you shouldn't touch. +This factory returns an instance of `Alert`. The `Alert` class has the following methods. Note that we have removed the methods that you shouldn't touch. |Method|Description| |---|---| @@ -781,7 +779,8 @@ The templating engine is [mustache]. General definition for the [mustache variab The following code would be within a rule type. As you can see `cpuUsage` will replace the state of the alert and `server` is the context for the alert to execute. The difference between the two is that `cpuUsage` will be accessible at the next execution. ``` -alertInstanceFactory('server_1') +alertFactory + .create('server_1') .replaceState({ cpuUsage: 80, }) diff --git a/x-pack/plugins/alerting/server/alert/alert.test.ts b/x-pack/plugins/alerting/server/alert/alert.test.ts new file mode 100644 index 0000000000000..83b82de904703 --- /dev/null +++ b/x-pack/plugins/alerting/server/alert/alert.test.ts @@ -0,0 +1,488 @@ +/* + * 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 sinon from 'sinon'; +import { Alert } from './alert'; +import { AlertInstanceState, AlertInstanceContext, DefaultActionGroupId } from '../../common'; + +let clock: sinon.SinonFakeTimers; + +beforeAll(() => { + clock = sinon.useFakeTimers(); +}); +beforeEach(() => clock.reset()); +afterAll(() => clock.restore()); + +describe('hasScheduledActions()', () => { + test('defaults to false', () => { + const alert = new Alert(); + expect(alert.hasScheduledActions()).toEqual(false); + }); + + test('returns true when scheduleActions is called', () => { + const alert = new Alert(); + alert.scheduleActions('default'); + expect(alert.hasScheduledActions()).toEqual(true); + }); +}); + +describe('isThrottled', () => { + test(`should throttle when group didn't change and throttle period is still active`, () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + clock.tick(30000); + alert.scheduleActions('default'); + expect(alert.isThrottled('1m')).toEqual(true); + }); + + test(`shouldn't throttle when group didn't change and throttle period expired`, () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + clock.tick(30000); + alert.scheduleActions('default'); + expect(alert.isThrottled('15s')).toEqual(false); + }); + + test(`shouldn't throttle when group changes`, () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + clock.tick(5000); + alert.scheduleActions('other-group'); + expect(alert.isThrottled('1m')).toEqual(false); + }); +}); + +describe('scheduledActionGroupOrSubgroupHasChanged()', () => { + test('should be false if no last scheduled and nothing scheduled', () => { + const alert = new Alert(); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); + }); + + test('should be false if group does not change', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert.scheduleActions('default'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); + }); + + test('should be false if group and subgroup does not change', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + subgroup: 'subgroup', + }, + }, + }); + alert.scheduleActionsWithSubGroup('default', 'subgroup'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); + }); + + test('should be false if group does not change and subgroup goes from undefined to defined', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert.scheduleActionsWithSubGroup('default', 'subgroup'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); + }); + + test('should be false if group does not change and subgroup goes from defined to undefined', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + subgroup: 'subgroup', + }, + }, + }); + alert.scheduleActions('default'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); + }); + + test('should be true if no last scheduled and has scheduled action', () => { + const alert = new Alert(); + alert.scheduleActions('default'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); + }); + + test('should be true if group does change', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert.scheduleActions('penguin'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); + }); + + test('should be true if group does change and subgroup does change', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + subgroup: 'subgroup', + }, + }, + }); + alert.scheduleActionsWithSubGroup('penguin', 'fish'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); + }); + + test('should be true if group does not change and subgroup does change', () => { + const alert = new Alert({ + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + subgroup: 'subgroup', + }, + }, + }); + alert.scheduleActionsWithSubGroup('default', 'fish'); + expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); + }); +}); + +describe('getScheduledActionOptions()', () => { + test('defaults to undefined', () => { + const alert = new Alert(); + expect(alert.getScheduledActionOptions()).toBeUndefined(); + }); +}); + +describe('unscheduleActions()', () => { + test('makes hasScheduledActions() return false', () => { + const alert = new Alert(); + alert.scheduleActions('default'); + expect(alert.hasScheduledActions()).toEqual(true); + alert.unscheduleActions(); + expect(alert.hasScheduledActions()).toEqual(false); + }); + + test('makes getScheduledActionOptions() return undefined', () => { + const alert = new Alert(); + alert.scheduleActions('default'); + expect(alert.getScheduledActionOptions()).toEqual({ + actionGroup: 'default', + context: {}, + state: {}, + }); + alert.unscheduleActions(); + expect(alert.getScheduledActionOptions()).toBeUndefined(); + }); +}); + +describe('getState()', () => { + test('returns state passed to constructor', () => { + const state = { foo: true }; + const alert = new Alert({ + state, + }); + expect(alert.getState()).toEqual(state); + }); +}); + +describe('scheduleActions()', () => { + test('makes hasScheduledActions() return true', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert.replaceState({ otherField: true }).scheduleActions('default', { field: true }); + expect(alert.hasScheduledActions()).toEqual(true); + }); + + test('makes isThrottled() return true when throttled', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert.replaceState({ otherField: true }).scheduleActions('default', { field: true }); + expect(alert.isThrottled('1m')).toEqual(true); + }); + + test('make isThrottled() return false when throttled expired', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + clock.tick(120000); + alert.replaceState({ otherField: true }).scheduleActions('default', { field: true }); + expect(alert.isThrottled('1m')).toEqual(false); + }); + + test('makes getScheduledActionOptions() return given options', () => { + const alert = new Alert({ + state: { foo: true }, + meta: {}, + }); + alert.replaceState({ otherField: true }).scheduleActions('default', { field: true }); + expect(alert.getScheduledActionOptions()).toEqual({ + actionGroup: 'default', + context: { field: true }, + state: { otherField: true }, + }); + }); + + test('cannot schdule for execution twice', () => { + const alert = new Alert(); + alert.scheduleActions('default', { field: true }); + expect(() => + alert.scheduleActions('default', { field: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Alert instance execution has already been scheduled, cannot schedule twice"` + ); + }); +}); + +describe('scheduleActionsWithSubGroup()', () => { + test('makes hasScheduledActions() return true', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert + .replaceState({ otherField: true }) + .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(alert.hasScheduledActions()).toEqual(true); + }); + + test('makes isThrottled() return true when throttled and subgroup is the same', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + subgroup: 'subgroup', + }, + }, + }); + alert + .replaceState({ otherField: true }) + .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(alert.isThrottled('1m')).toEqual(true); + }); + + test('makes isThrottled() return true when throttled and last schedule had no subgroup', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert + .replaceState({ otherField: true }) + .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(alert.isThrottled('1m')).toEqual(true); + }); + + test('makes isThrottled() return false when throttled and subgroup is the different', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + subgroup: 'prev-subgroup', + }, + }, + }); + alert + .replaceState({ otherField: true }) + .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(alert.isThrottled('1m')).toEqual(false); + }); + + test('make isThrottled() return false when throttled expired', () => { + const alert = new Alert({ + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + clock.tick(120000); + alert + .replaceState({ otherField: true }) + .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(alert.isThrottled('1m')).toEqual(false); + }); + + test('makes getScheduledActionOptions() return given options', () => { + const alert = new Alert({ + state: { foo: true }, + meta: {}, + }); + alert + .replaceState({ otherField: true }) + .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(alert.getScheduledActionOptions()).toEqual({ + actionGroup: 'default', + subgroup: 'subgroup', + context: { field: true }, + state: { otherField: true }, + }); + }); + + test('cannot schdule for execution twice', () => { + const alert = new Alert(); + alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(() => + alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Alert instance execution has already been scheduled, cannot schedule twice"` + ); + }); + + test('cannot schdule for execution twice with different subgroups', () => { + const alert = new Alert(); + alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); + expect(() => + alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Alert instance execution has already been scheduled, cannot schedule twice"` + ); + }); + + test('cannot schdule for execution twice whether there are subgroups', () => { + const alert = new Alert(); + alert.scheduleActions('default', { field: true }); + expect(() => + alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) + ).toThrowErrorMatchingInlineSnapshot( + `"Alert instance execution has already been scheduled, cannot schedule twice"` + ); + }); +}); + +describe('replaceState()', () => { + test('replaces previous state', () => { + const alert = new Alert({ + state: { foo: true }, + }); + alert.replaceState({ bar: true }); + expect(alert.getState()).toEqual({ bar: true }); + alert.replaceState({ baz: true }); + expect(alert.getState()).toEqual({ baz: true }); + }); +}); + +describe('updateLastScheduledActions()', () => { + test('replaces previous lastScheduledActions', () => { + const alert = new Alert({ + meta: {}, + }); + alert.updateLastScheduledActions('default'); + expect(alert.toJSON()).toEqual({ + state: {}, + meta: { + lastScheduledActions: { + date: new Date().toISOString(), + group: 'default', + }, + }, + }); + }); +}); + +describe('toJSON', () => { + test('only serializes state and meta', () => { + const alertInstance = new Alert( + { + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + } + ); + expect(JSON.stringify(alertInstance)).toEqual( + '{"state":{"foo":true},"meta":{"lastScheduledActions":{"date":"1970-01-01T00:00:00.000Z","group":"default"}}}' + ); + }); +}); + +describe('toRaw', () => { + test('returns unserialised underlying state and meta', () => { + const raw = { + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }; + const alertInstance = new Alert( + raw + ); + expect(alertInstance.toRaw()).toEqual(raw); + }); +}); diff --git a/x-pack/plugins/alerting/server/alert_instance/alert_instance.ts b/x-pack/plugins/alerting/server/alert/alert.ts similarity index 97% rename from x-pack/plugins/alerting/server/alert_instance/alert_instance.ts rename to x-pack/plugins/alerting/server/alert/alert.ts index b41a4e551040c..d34aa68ac1a11 100644 --- a/x-pack/plugins/alerting/server/alert_instance/alert_instance.ts +++ b/x-pack/plugins/alerting/server/alert/alert.ts @@ -27,16 +27,16 @@ interface ScheduledExecutionOptions< state: State; } -export type PublicAlertInstance< +export type PublicAlert< State extends AlertInstanceState = AlertInstanceState, Context extends AlertInstanceContext = AlertInstanceContext, ActionGroupIds extends string = DefaultActionGroupId > = Pick< - AlertInstance, + Alert, 'getState' | 'replaceState' | 'scheduleActions' | 'scheduleActionsWithSubGroup' >; -export class AlertInstance< +export class Alert< State extends AlertInstanceState = AlertInstanceState, Context extends AlertInstanceContext = AlertInstanceContext, ActionGroupIds extends string = never diff --git a/x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts b/x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts similarity index 61% rename from x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts rename to x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts index 6518305dcd109..ecb1a10bbac42 100644 --- a/x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts +++ b/x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts @@ -6,8 +6,8 @@ */ import sinon from 'sinon'; -import { AlertInstance } from './alert_instance'; -import { createAlertInstanceFactory } from './create_alert_instance_factory'; +import { Alert } from './alert'; +import { createAlertFactory } from './create_alert_factory'; let clock: sinon.SinonFakeTimers; @@ -17,9 +17,9 @@ beforeAll(() => { beforeEach(() => clock.reset()); afterAll(() => clock.restore()); -test('creates new instances for ones not passed in', () => { - const alertInstanceFactory = createAlertInstanceFactory({}); - const result = alertInstanceFactory('1'); +test('creates new alerts for ones not passed in', () => { + const alertFactory = createAlertFactory({ alerts: {} }); + const result = alertFactory.create('1'); expect(result).toMatchInlineSnapshot(` Object { "meta": Object {}, @@ -28,15 +28,17 @@ test('creates new instances for ones not passed in', () => { `); }); -test('reuses existing instances', () => { - const alertInstance = new AlertInstance({ +test('reuses existing alerts', () => { + const alert = new Alert({ state: { foo: true }, meta: { lastScheduledActions: { group: 'default', date: new Date() } }, }); - const alertInstanceFactory = createAlertInstanceFactory({ - '1': alertInstance, + const alertFactory = createAlertFactory({ + alerts: { + '1': alert, + }, }); - const result = alertInstanceFactory('1'); + const result = alertFactory.create('1'); expect(result).toMatchInlineSnapshot(` Object { "meta": Object { @@ -52,11 +54,11 @@ test('reuses existing instances', () => { `); }); -test('mutates given instances', () => { - const alertInstances = {}; - const alertInstanceFactory = createAlertInstanceFactory(alertInstances); - alertInstanceFactory('1'); - expect(alertInstances).toMatchInlineSnapshot(` +test('mutates given alerts', () => { + const alerts = {}; + const alertFactory = createAlertFactory({ alerts }); + alertFactory.create('1'); + expect(alerts).toMatchInlineSnapshot(` Object { "1": Object { "meta": Object {}, diff --git a/x-pack/plugins/alerting/server/alert/create_alert_factory.ts b/x-pack/plugins/alerting/server/alert/create_alert_factory.ts new file mode 100644 index 0000000000000..07f4dbc7b20ea --- /dev/null +++ b/x-pack/plugins/alerting/server/alert/create_alert_factory.ts @@ -0,0 +1,33 @@ +/* + * 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 { AlertInstanceContext, AlertInstanceState } from '../types'; +import { Alert } from './alert'; + +export interface CreateAlertFactoryOpts< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string +> { + alerts: Record>; +} + +export function createAlertFactory< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string +>({ alerts }: CreateAlertFactoryOpts) { + return { + create: (id: string): Alert => { + if (!alerts[id]) { + alerts[id] = new Alert(); + } + + return alerts[id]; + }, + }; +} diff --git a/x-pack/plugins/alerting/server/alert_instance/index.ts b/x-pack/plugins/alerting/server/alert/index.ts similarity index 57% rename from x-pack/plugins/alerting/server/alert_instance/index.ts rename to x-pack/plugins/alerting/server/alert/index.ts index 7b5dd064c5dca..5e1a9ee626b57 100644 --- a/x-pack/plugins/alerting/server/alert_instance/index.ts +++ b/x-pack/plugins/alerting/server/alert/index.ts @@ -5,6 +5,6 @@ * 2.0. */ -export type { PublicAlertInstance } from './alert_instance'; -export { AlertInstance } from './alert_instance'; -export { createAlertInstanceFactory } from './create_alert_instance_factory'; +export type { PublicAlert } from './alert'; +export { Alert } from './alert'; +export { createAlertFactory } from './create_alert_factory'; diff --git a/x-pack/plugins/alerting/server/alert_instance/alert_instance.test.ts b/x-pack/plugins/alerting/server/alert_instance/alert_instance.test.ts deleted file mode 100644 index 68fed6aa7d3fd..0000000000000 --- a/x-pack/plugins/alerting/server/alert_instance/alert_instance.test.ts +++ /dev/null @@ -1,604 +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 sinon from 'sinon'; -import { AlertInstance } from './alert_instance'; -import { AlertInstanceState, AlertInstanceContext, DefaultActionGroupId } from '../../common'; - -let clock: sinon.SinonFakeTimers; - -beforeAll(() => { - clock = sinon.useFakeTimers(); -}); -beforeEach(() => clock.reset()); -afterAll(() => clock.restore()); - -describe('hasScheduledActions()', () => { - test('defaults to false', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - expect(alertInstance.hasScheduledActions()).toEqual(false); - }); - - test('returns true when scheduleActions is called', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActions('default'); - expect(alertInstance.hasScheduledActions()).toEqual(true); - }); -}); - -describe('isThrottled', () => { - test(`should throttle when group didn't change and throttle period is still active`, () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - clock.tick(30000); - alertInstance.scheduleActions('default'); - expect(alertInstance.isThrottled('1m')).toEqual(true); - }); - - test(`shouldn't throttle when group didn't change and throttle period expired`, () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - clock.tick(30000); - alertInstance.scheduleActions('default'); - expect(alertInstance.isThrottled('15s')).toEqual(false); - }); - - test(`shouldn't throttle when group changes`, () => { - const alertInstance = new AlertInstance({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - clock.tick(5000); - alertInstance.scheduleActions('other-group'); - expect(alertInstance.isThrottled('1m')).toEqual(false); - }); -}); - -describe('scheduledActionGroupOrSubgroupHasChanged()', () => { - test('should be false if no last scheduled and nothing scheduled', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); - }); - - test('should be false if group does not change', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance.scheduleActions('default'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); - }); - - test('should be false if group and subgroup does not change', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'subgroup', - }, - }, - }); - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); - }); - - test('should be false if group does not change and subgroup goes from undefined to defined', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); - }); - - test('should be false if group does not change and subgroup goes from defined to undefined', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'subgroup', - }, - }, - }); - alertInstance.scheduleActions('default'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); - }); - - test('should be true if no last scheduled and has scheduled action', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActions('default'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); - }); - - test('should be true if group does change', () => { - const alertInstance = new AlertInstance({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance.scheduleActions('penguin'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); - }); - - test('should be true if group does change and subgroup does change', () => { - const alertInstance = new AlertInstance({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'subgroup', - }, - }, - }); - alertInstance.scheduleActionsWithSubGroup('penguin', 'fish'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); - }); - - test('should be true if group does not change and subgroup does change', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'subgroup', - }, - }, - }); - alertInstance.scheduleActionsWithSubGroup('default', 'fish'); - expect(alertInstance.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); - }); -}); - -describe('getScheduledActionOptions()', () => { - test('defaults to undefined', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - expect(alertInstance.getScheduledActionOptions()).toBeUndefined(); - }); -}); - -describe('unscheduleActions()', () => { - test('makes hasScheduledActions() return false', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActions('default'); - expect(alertInstance.hasScheduledActions()).toEqual(true); - alertInstance.unscheduleActions(); - expect(alertInstance.hasScheduledActions()).toEqual(false); - }); - - test('makes getScheduledActionOptions() return undefined', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActions('default'); - expect(alertInstance.getScheduledActionOptions()).toEqual({ - actionGroup: 'default', - context: {}, - state: {}, - }); - alertInstance.unscheduleActions(); - expect(alertInstance.getScheduledActionOptions()).toBeUndefined(); - }); -}); - -describe('getState()', () => { - test('returns state passed to constructor', () => { - const state = { foo: true }; - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ state }); - expect(alertInstance.getState()).toEqual(state); - }); -}); - -describe('scheduleActions()', () => { - test('makes hasScheduledActions() return true', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance.replaceState({ otherField: true }).scheduleActions('default', { field: true }); - expect(alertInstance.hasScheduledActions()).toEqual(true); - }); - - test('makes isThrottled() return true when throttled', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance.replaceState({ otherField: true }).scheduleActions('default', { field: true }); - expect(alertInstance.isThrottled('1m')).toEqual(true); - }); - - test('make isThrottled() return false when throttled expired', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - clock.tick(120000); - alertInstance.replaceState({ otherField: true }).scheduleActions('default', { field: true }); - expect(alertInstance.isThrottled('1m')).toEqual(false); - }); - - test('makes getScheduledActionOptions() return given options', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ state: { foo: true }, meta: {} }); - alertInstance.replaceState({ otherField: true }).scheduleActions('default', { field: true }); - expect(alertInstance.getScheduledActionOptions()).toEqual({ - actionGroup: 'default', - context: { field: true }, - state: { otherField: true }, - }); - }); - - test('cannot schdule for execution twice', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActions('default', { field: true }); - expect(() => - alertInstance.scheduleActions('default', { field: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Alert instance execution has already been scheduled, cannot schedule twice"` - ); - }); -}); - -describe('scheduleActionsWithSubGroup()', () => { - test('makes hasScheduledActions() return true', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alertInstance.hasScheduledActions()).toEqual(true); - }); - - test('makes isThrottled() return true when throttled and subgroup is the same', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'subgroup', - }, - }, - }); - alertInstance - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alertInstance.isThrottled('1m')).toEqual(true); - }); - - test('makes isThrottled() return true when throttled and last schedule had no subgroup', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - alertInstance - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alertInstance.isThrottled('1m')).toEqual(true); - }); - - test('makes isThrottled() return false when throttled and subgroup is the different', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - subgroup: 'prev-subgroup', - }, - }, - }); - alertInstance - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alertInstance.isThrottled('1m')).toEqual(false); - }); - - test('make isThrottled() return false when throttled expired', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - clock.tick(120000); - alertInstance - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alertInstance.isThrottled('1m')).toEqual(false); - }); - - test('makes getScheduledActionOptions() return given options', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ state: { foo: true }, meta: {} }); - alertInstance - .replaceState({ otherField: true }) - .scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(alertInstance.getScheduledActionOptions()).toEqual({ - actionGroup: 'default', - subgroup: 'subgroup', - context: { field: true }, - state: { otherField: true }, - }); - }); - - test('cannot schdule for execution twice', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(() => - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Alert instance execution has already been scheduled, cannot schedule twice"` - ); - }); - - test('cannot schdule for execution twice with different subgroups', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); - expect(() => - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Alert instance execution has already been scheduled, cannot schedule twice"` - ); - }); - - test('cannot schdule for execution twice whether there are subgroups', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(); - alertInstance.scheduleActions('default', { field: true }); - expect(() => - alertInstance.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) - ).toThrowErrorMatchingInlineSnapshot( - `"Alert instance execution has already been scheduled, cannot schedule twice"` - ); - }); -}); - -describe('replaceState()', () => { - test('replaces previous state', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ state: { foo: true } }); - alertInstance.replaceState({ bar: true }); - expect(alertInstance.getState()).toEqual({ bar: true }); - alertInstance.replaceState({ baz: true }); - expect(alertInstance.getState()).toEqual({ baz: true }); - }); -}); - -describe('updateLastScheduledActions()', () => { - test('replaces previous lastScheduledActions', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ meta: {} }); - alertInstance.updateLastScheduledActions('default'); - expect(alertInstance.toJSON()).toEqual({ - state: {}, - meta: { - lastScheduledActions: { - date: new Date().toISOString(), - group: 'default', - }, - }, - }); - }); -}); - -describe('toJSON', () => { - test('only serializes state and meta', () => { - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >({ - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }); - expect(JSON.stringify(alertInstance)).toEqual( - '{"state":{"foo":true},"meta":{"lastScheduledActions":{"date":"1970-01-01T00:00:00.000Z","group":"default"}}}' - ); - }); -}); - -describe('toRaw', () => { - test('returns unserialised underlying state and meta', () => { - const raw = { - state: { foo: true }, - meta: { - lastScheduledActions: { - date: new Date(), - group: 'default', - }, - }, - }; - const alertInstance = new AlertInstance< - AlertInstanceState, - AlertInstanceContext, - DefaultActionGroupId - >(raw); - expect(alertInstance.toRaw()).toEqual(raw); - }); -}); diff --git a/x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.ts b/x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.ts deleted file mode 100644 index 2faaff157fd82..0000000000000 --- a/x-pack/plugins/alerting/server/alert_instance/create_alert_instance_factory.ts +++ /dev/null @@ -1,23 +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 { AlertInstanceContext, AlertInstanceState } from '../types'; -import { AlertInstance } from './alert_instance'; - -export function createAlertInstanceFactory< - InstanceState extends AlertInstanceState, - InstanceContext extends AlertInstanceContext, - ActionGroupIds extends string ->(alertInstances: Record>) { - return (id: string): AlertInstance => { - if (!alertInstances[id]) { - alertInstances[id] = new AlertInstance(); - } - - return alertInstances[id]; - }; -} diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index 36ee1f8ee9676..63e8df5488895 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -32,7 +32,7 @@ export type { export { DEFAULT_MAX_EPHEMERAL_ACTIONS_PER_ALERT } from './config'; export type { PluginSetupContract, PluginStartContract } from './plugin'; export type { FindResult } from './rules_client'; -export type { PublicAlertInstance as AlertInstance } from './alert_instance'; +export type { PublicAlert as Alert } from './alert'; export { parseDuration } from './lib'; export { getEsErrorMessage } from './lib/errors'; export type { diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index c4702f796ad8e..afbc3ef9cec43 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -7,7 +7,7 @@ import { rulesClientMock } from './rules_client.mock'; import { PluginSetupContract, PluginStartContract } from './plugin'; -import { AlertInstance } from './alert_instance'; +import { Alert } from './alert'; import { elasticsearchServiceMock, savedObjectsClientMock, @@ -37,30 +37,33 @@ const createStartMock = () => { export type AlertInstanceMock< State extends AlertInstanceState = AlertInstanceState, Context extends AlertInstanceContext = AlertInstanceContext -> = jest.Mocked>; -const createAlertInstanceFactoryMock = < - InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext ->() => { - const mock = { - hasScheduledActions: jest.fn(), - isThrottled: jest.fn(), - getScheduledActionOptions: jest.fn(), - unscheduleActions: jest.fn(), - getState: jest.fn(), - scheduleActions: jest.fn(), - replaceState: jest.fn(), - updateLastScheduledActions: jest.fn(), - toJSON: jest.fn(), - toRaw: jest.fn(), - }; +> = jest.Mocked>; + +const createAlertFactoryMock = { + create: < + InstanceState extends AlertInstanceState = AlertInstanceState, + InstanceContext extends AlertInstanceContext = AlertInstanceContext + >() => { + const mock = { + hasScheduledActions: jest.fn(), + isThrottled: jest.fn(), + getScheduledActionOptions: jest.fn(), + unscheduleActions: jest.fn(), + getState: jest.fn(), + scheduleActions: jest.fn(), + replaceState: jest.fn(), + updateLastScheduledActions: jest.fn(), + toJSON: jest.fn(), + toRaw: jest.fn(), + }; - // support chaining - mock.replaceState.mockReturnValue(mock); - mock.unscheduleActions.mockReturnValue(mock); - mock.scheduleActions.mockReturnValue(mock); + // support chaining + mock.replaceState.mockReturnValue(mock); + mock.unscheduleActions.mockReturnValue(mock); + mock.scheduleActions.mockReturnValue(mock); - return mock as unknown as AlertInstanceMock; + return mock as unknown as AlertInstanceMock; + }, }; const createAbortableSearchClientMock = () => { @@ -82,11 +85,11 @@ const createAlertServicesMock = < InstanceState extends AlertInstanceState = AlertInstanceState, InstanceContext extends AlertInstanceContext = AlertInstanceContext >() => { - const alertInstanceFactoryMock = createAlertInstanceFactoryMock(); + const alertFactoryMockCreate = createAlertFactoryMock.create(); return { - alertInstanceFactory: jest - .fn>, [string]>() - .mockReturnValue(alertInstanceFactoryMock), + alertFactory: { + create: jest.fn().mockReturnValue(alertFactoryMockCreate), + }, savedObjectsClient: savedObjectsClientMock.create(), scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), shouldWriteAlerts: () => true, @@ -97,7 +100,7 @@ const createAlertServicesMock = < export type AlertServicesMock = ReturnType; export const alertsMock = { - createAlertInstanceFactory: createAlertInstanceFactoryMock, + createAlertFactory: createAlertFactoryMock, createSetup: createSetupMock, createStart: createStartMock, createAlertServices: createAlertServicesMock, diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 9100772a806e8..63e35583bc9a1 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -22,23 +22,23 @@ import { import { esKuery } from '../../../../../src/plugins/data/server'; import { ActionsClient, ActionsAuthorization } from '../../../actions/server'; import { - Alert, - PartialAlert, + Alert as Rule, + PartialAlert as PartialRule, RawRule, RuleTypeRegistry, - AlertAction, + AlertAction as RuleAction, IntervalSchedule, - SanitizedAlert, + SanitizedAlert as SanitizedRule, RuleTaskState, AlertSummary, - AlertExecutionStatusValues, - AlertNotifyWhenType, - AlertTypeParams, + AlertExecutionStatusValues as RuleExecutionStatusValues, + AlertNotifyWhenType as RuleNotifyWhenType, + AlertTypeParams as RuleTypeParams, ResolvedSanitizedRule, - AlertWithLegacyId, + AlertWithLegacyId as RuleWithLegacyId, SanitizedRuleWithLegacyId, - PartialAlertWithLegacyId, - RawAlertInstance, + PartialAlertWithLegacyId as PartialRuleWithLegacyId, + RawAlertInstance as RawAlert, } from '../types'; import { validateRuleTypeParams, ruleExecutionStatusFromRaw, getAlertNotifyWhenType } from '../lib'; import { @@ -74,7 +74,7 @@ import { ruleAuditEvent, RuleAuditAction } from './audit_events'; import { KueryNode, nodeBuilder } from '../../../../../src/plugins/data/common'; import { mapSortField, validateOperationOnAttributes } from './lib'; import { getRuleExecutionStatusPending } from '../lib/rule_execution_status'; -import { AlertInstance } from '../alert_instance'; +import { Alert } from '../alert'; import { EVENT_LOG_ACTIONS } from '../plugin'; import { createAlertEventLogRecordObject } from '../lib/create_alert_event_log_record_object'; import { getDefaultRuleMonitoring } from '../task_runner/task_runner'; @@ -82,7 +82,7 @@ import { getDefaultRuleMonitoring } from '../task_runner/task_runner'; export interface RegistryAlertTypeWithAuth extends RegistryRuleType { authorizedConsumers: string[]; } -type NormalizedAlertAction = Omit; +type NormalizedAlertAction = Omit; export type CreateAPIKeyResult = | { apiKeysEnabled: false } | { apiKeysEnabled: true; result: SecurityPluginGrantAPIKeyResult }; @@ -174,16 +174,16 @@ export interface AggregateResult { ruleMutedStatus?: { muted: number; unmuted: number }; } -export interface FindResult { +export interface FindResult { page: number; perPage: number; total: number; - data: Array>; + data: Array>; } -export interface CreateOptions { +export interface CreateOptions { data: Omit< - Alert, + Rule, | 'id' | 'createdBy' | 'updatedBy' @@ -202,7 +202,7 @@ export interface CreateOptions { }; } -export interface UpdateOptions { +export interface UpdateOptions { id: string; data: { name: string; @@ -211,7 +211,7 @@ export interface UpdateOptions { actions: NormalizedAlertAction[]; params: Params; throttle: string | null; - notifyWhen: AlertNotifyWhenType | null; + notifyWhen: RuleNotifyWhenType | null; }; } @@ -248,7 +248,7 @@ export class RulesClient { private readonly kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version']; private readonly auditLogger?: AuditLogger; private readonly eventLogger?: IEventLogger; - private readonly fieldsToExcludeFromPublicApi: Array = ['monitoring']; + private readonly fieldsToExcludeFromPublicApi: Array = ['monitoring']; constructor({ ruleTypeRegistry, @@ -286,10 +286,10 @@ export class RulesClient { this.eventLogger = eventLogger; } - public async create({ + public async create({ data, options, - }: CreateOptions): Promise> { + }: CreateOptions): Promise> { const id = options?.id || SavedObjectsUtils.generateId(); try { @@ -432,7 +432,7 @@ export class RulesClient { ); } - public async get({ + public async get({ id, includeLegacyId = false, excludeFromPublicApi = false, @@ -440,7 +440,7 @@ export class RulesClient { id: string; includeLegacyId?: boolean; excludeFromPublicApi?: boolean; - }): Promise | SanitizedRuleWithLegacyId> { + }): Promise | SanitizedRuleWithLegacyId> { const result = await this.unsecuredSavedObjectsClient.get('alert', id); try { await this.authorization.ensureAuthorized({ @@ -475,7 +475,7 @@ export class RulesClient { ); } - public async resolve({ + public async resolve({ id, includeLegacyId, }: { @@ -612,7 +612,7 @@ export class RulesClient { }); } - public async find({ + public async find({ options: { fields, ...options } = {}, excludeFromPublicApi = false, }: { options?: FindOptions; excludeFromPublicApi?: boolean } = {}): Promise> { @@ -762,7 +762,7 @@ export class RulesClient { }, }; - for (const key of AlertExecutionStatusValues) { + for (const key of RuleExecutionStatusValues) { placeholder.alertExecutionStatus[key] = 0; } @@ -783,7 +783,7 @@ export class RulesClient { }; // Fill missing keys with zeroes - for (const key of AlertExecutionStatusValues) { + for (const key of RuleExecutionStatusValues) { if (!ret.alertExecutionStatus.hasOwnProperty(key)) { ret.alertExecutionStatus[key] = 0; } @@ -878,10 +878,10 @@ export class RulesClient { return removeResult; } - public async update({ + public async update({ id, data, - }: UpdateOptions): Promise> { + }: UpdateOptions): Promise> { return await retryIfConflicts( this.logger, `rulesClient.update('${id}')`, @@ -889,10 +889,10 @@ export class RulesClient { ); } - private async updateWithOCC({ + private async updateWithOCC({ id, data, - }: UpdateOptions): Promise> { + }: UpdateOptions): Promise> { let alertSavedObject: SavedObject; try { @@ -974,10 +974,10 @@ export class RulesClient { return updateResult; } - private async updateAlert( + private async updateAlert( { id, data }: UpdateOptions, { attributes, version }: SavedObject - ): Promise> { + ): Promise> { const ruleType = this.ruleTypeRegistry.get(attributes.alertTypeId); // Validate @@ -1048,7 +1048,7 @@ export class RulesClient { throw e; } - return this.getPartialAlertFromRaw( + return this.getPartialRuleFromRaw( id, ruleType, updatedObject.attributes, @@ -1332,12 +1332,12 @@ export class RulesClient { try { const { state } = taskInstanceToAlertTaskInstance( await this.taskManager.get(attributes.scheduledTaskId), - attributes as unknown as SanitizedAlert + attributes as unknown as SanitizedRule ); - const recoveredAlertInstances = mapValues, AlertInstance>( + const recoveredAlertInstances = mapValues, Alert>( state.alertInstances ?? {}, - (rawAlertInstance) => new AlertInstance(rawAlertInstance) + (rawAlertInstance) => new Alert(rawAlertInstance) ); const recoveredAlertInstanceIds = Object.keys(recoveredAlertInstances); @@ -1568,7 +1568,7 @@ export class RulesClient { } private async muteInstanceWithOCC({ alertId, alertInstanceId }: MuteOptions) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( 'alert', alertId ); @@ -1636,7 +1636,7 @@ export class RulesClient { alertId: string; alertInstanceId: string; }) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( 'alert', alertId ); @@ -1751,22 +1751,22 @@ export class RulesClient { ...omit(action, 'actionRef'), id: reference.id, }; - }) as Alert['actions']; + }) as Rule['actions']; } - private getAlertFromRaw( + private getAlertFromRaw( id: string, ruleTypeId: string, rawRule: RawRule, references: SavedObjectReference[] | undefined, includeLegacyId: boolean = false, excludeFromPublicApi: boolean = false - ): Alert | AlertWithLegacyId { + ): Rule | RuleWithLegacyId { const ruleType = this.ruleTypeRegistry.get(ruleTypeId); // In order to support the partial update API of Saved Objects we have to support // partial updates of an Alert, but when we receive an actual RawRule, it is safe // to cast the result to an Alert - const res = this.getPartialAlertFromRaw( + const res = this.getPartialRuleFromRaw( id, ruleType, rawRule, @@ -1776,13 +1776,13 @@ export class RulesClient { ); // include to result because it is for internal rules client usage if (includeLegacyId) { - return res as AlertWithLegacyId; + return res as RuleWithLegacyId; } // exclude from result because it is an internal variable - return omit(res, ['legacyId']) as Alert; + return omit(res, ['legacyId']) as Rule; } - private getPartialAlertFromRaw( + private getPartialRuleFromRaw( id: string, ruleType: UntypedNormalizedRuleType, { @@ -1801,7 +1801,7 @@ export class RulesClient { references: SavedObjectReference[] | undefined, includeLegacyId: boolean = false, excludeFromPublicApi: boolean = false - ): PartialAlert | PartialAlertWithLegacyId { + ): PartialRule | PartialRuleWithLegacyId { const rule = { id, notifyWhen, @@ -1820,8 +1820,8 @@ export class RulesClient { }; return includeLegacyId - ? ({ ...rule, legacyId } as PartialAlertWithLegacyId) - : (rule as PartialAlert); + ? ({ ...rule, legacyId } as PartialRuleWithLegacyId) + : (rule as PartialRule); } private async validateActions( @@ -1873,8 +1873,8 @@ export class RulesClient { } private async extractReferences< - Params extends AlertTypeParams, - ExtractedParams extends AlertTypeParams + Params extends RuleTypeParams, + ExtractedParams extends RuleTypeParams >( ruleType: UntypedNormalizedRuleType, ruleActions: NormalizedAlertAction[], @@ -1909,8 +1909,8 @@ export class RulesClient { } private injectReferencesIntoParams< - Params extends AlertTypeParams, - ExtractedParams extends AlertTypeParams + Params extends RuleTypeParams, + ExtractedParams extends RuleTypeParams >( ruleId: string, ruleType: UntypedNormalizedRuleType, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 402cc3951d39b..b5a98af23d74b 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -309,7 +309,7 @@ describe('Task Runner', () => { }, ] `); - expect(call.services.alertInstanceFactory).toBeTruthy(); + expect(call.services.alertFactory.create).toBeTruthy(); expect(call.services.scopedClusterClient).toBeTruthy(); expect(call.services).toBeTruthy(); @@ -427,8 +427,8 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices - .alertInstanceFactory('1') + executorServices.alertFactory + .create('1') .scheduleActionsWithSubGroup('default', 'subDefault'); } ); @@ -708,7 +708,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -934,8 +934,8 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); - executorServices.alertInstanceFactory('2').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); + executorServices.alertFactory.create('2').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -991,7 +991,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -1192,7 +1192,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -1268,8 +1268,8 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices - .alertInstanceFactory('1') + executorServices.alertFactory + .create('1') .scheduleActionsWithSubGroup('default', 'subgroup1'); } ); @@ -1350,7 +1350,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -1672,7 +1672,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -2080,10 +2080,10 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); // create an instance, but don't schedule any actions, so it doesn't go active - executorServices.alertInstanceFactory('3'); + executorServices.alertFactory.create('3'); } ); const taskRunner = new TaskRunner( @@ -2186,7 +2186,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -2297,7 +2297,7 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const date = new Date().toISOString(); @@ -3692,8 +3692,8 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); - executorServices.alertInstanceFactory('2').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); + executorServices.alertFactory.create('2').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -4006,8 +4006,8 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); - executorServices.alertInstanceFactory('2').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); + executorServices.alertFactory.create('2').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -4251,8 +4251,8 @@ describe('Task Runner', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); - executorServices.alertInstanceFactory('2').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); + executorServices.alertFactory.create('2').scheduleActions('default'); } ); const taskRunner = new TaskRunner( @@ -5035,7 +5035,7 @@ describe('Task Runner', () => { }, ] `); - expect(call.services.alertInstanceFactory).toBeTruthy(); + expect(call.services.alertFactory.create).toBeTruthy(); expect(call.services.scopedClusterClient).toBeTruthy(); expect(call.services).toBeTruthy(); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 785a68e1a24b9..9b77ec7f8dc72 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -15,7 +15,7 @@ import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance, throwUnrecoverableError } from '../../../task_manager/server'; import { createExecutionHandler, ExecutionHandler } from './create_execution_handler'; -import { AlertInstance, createAlertInstanceFactory } from '../alert_instance'; +import { Alert as CreatedAlert, createAlertFactory } from '../alert'; import { validateRuleTypeParams, executionStatusFromState, @@ -285,7 +285,7 @@ export class TaskRunner< async executeAlert( alertId: string, - alert: AlertInstance, + alert: CreatedAlert, executionHandler: ExecutionHandler ) { const { @@ -333,8 +333,8 @@ export class TaskRunner< const alerts = mapValues< Record, - AlertInstance - >(alertRawInstances, (rawAlert) => new AlertInstance(rawAlert)); + CreatedAlert + >(alertRawInstances, (rawAlert) => new CreatedAlert(rawAlert)); const originalAlerts = cloneDeep(alerts); const originalAlertIds = new Set(Object.keys(originalAlerts)); @@ -358,11 +358,13 @@ export class TaskRunner< executionId: this.executionId, services: { ...services, - alertInstanceFactory: createAlertInstanceFactory< + alertFactory: createAlertFactory< InstanceState, InstanceContext, WithoutReservedActionGroups - >(alerts), + >({ + alerts, + }), shouldWriteAlerts: () => this.shouldLogAndScheduleActionsForAlerts(), shouldStopExecution: () => this.cancelled, search: createAbortableEsClientFactory({ @@ -420,11 +422,11 @@ export class TaskRunner< // Cleanup alerts that are no longer scheduling actions to avoid over populating the alertInstances object const alertsWithScheduledActions = pickBy( alerts, - (alert: AlertInstance) => alert.hasScheduledActions() + (alert: CreatedAlert) => alert.hasScheduledActions() ); const recoveredAlerts = pickBy( alerts, - (alert: AlertInstance, id) => + (alert: CreatedAlert, id) => !alert.hasScheduledActions() && originalAlertIds.has(id) ); @@ -478,7 +480,7 @@ export class TaskRunner< const alertsToExecute = notifyWhen === 'onActionGroupChange' ? Object.entries(alertsWithScheduledActions).filter( - ([alertName, alert]: [string, AlertInstance]) => { + ([alertName, alert]: [string, CreatedAlert]) => { const shouldExecuteAction = alert.scheduledActionGroupOrSubgroupHasChanged(); if (!shouldExecuteAction) { this.logger.debug( @@ -489,7 +491,7 @@ export class TaskRunner< } ) : Object.entries(alertsWithScheduledActions).filter( - ([alertName, alert]: [string, AlertInstance]) => { + ([alertName, alert]: [string, CreatedAlert]) => { const throttled = alert.isThrottled(throttle); const muted = mutedAlertIdsSet.has(alertName); const shouldExecuteAction = !throttled && !muted; @@ -506,7 +508,7 @@ export class TaskRunner< const allTriggeredActions = await Promise.all( alertsToExecute.map( - ([alertId, alert]: [string, AlertInstance]) => + ([alertId, alert]: [string, CreatedAlert]) => this.executeAlert(alertId, alert, executionHandler) ) ); @@ -533,7 +535,7 @@ export class TaskRunner< triggeredActions, alertTypeState: updatedRuleTypeState || undefined, alertInstances: mapValues< - Record>, + Record>, RawAlertInstance >(alertsWithScheduledActions, (alert) => alert.toRaw()), }; @@ -910,9 +912,9 @@ interface TrackAlertDurationsParams< InstanceState extends AlertInstanceState, InstanceContext extends AlertInstanceContext > { - originalAlerts: Dictionary>; - currentAlerts: Dictionary>; - recoveredAlerts: Dictionary>; + originalAlerts: Dictionary>; + currentAlerts: Dictionary>; + recoveredAlerts: Dictionary>; } function trackAlertDurations< @@ -967,9 +969,9 @@ interface GenerateNewAndRecoveredAlertEventsParams< > { eventLogger: IEventLogger; executionId: string; - originalAlerts: Dictionary>; - currentAlerts: Dictionary>; - recoveredAlerts: Dictionary>; + originalAlerts: Dictionary>; + currentAlerts: Dictionary>; + recoveredAlerts: Dictionary>; ruleId: string; ruleLabel: string; namespace: string | undefined; @@ -1117,7 +1119,7 @@ interface ScheduleActionsForRecoveredAlertsParams< > { logger: Logger; recoveryActionGroup: ActionGroup; - recoveredAlerts: Dictionary>; + recoveredAlerts: Dictionary>; executionHandler: ExecutionHandler; mutedAlertIdsSet: Set; ruleLabel: string; @@ -1173,8 +1175,8 @@ interface LogActiveAndRecoveredAlertsParams< RecoveryActionGroupId extends string > { logger: Logger; - activeAlerts: Dictionary>; - recoveredAlerts: Dictionary>; + activeAlerts: Dictionary>; + recoveredAlerts: Dictionary>; ruleLabel: string; } diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts index 47f888fc71136..f4b67935f7249 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -359,7 +359,7 @@ describe('Task Runner Cancel', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); // setting cancelAlertsOnRuleTimeout to false here @@ -393,7 +393,7 @@ describe('Task Runner Cancel', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); // setting cancelAlertsOnRuleTimeout for ruleType to false here @@ -427,7 +427,7 @@ describe('Task Runner Cancel', () => { AlertInstanceContext, string >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertFactory.create('1').scheduleActions('default'); } ); const taskRunner = new TaskRunner( diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index de6649bb44891..9d6302774f889 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -7,7 +7,7 @@ import type { IRouter, RequestHandlerContext, SavedObjectReference } from 'src/core/server'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { PublicAlertInstance } from './alert_instance'; +import { PublicAlert } from './alert'; import { RuleTypeRegistry as OrigruleTypeRegistry } from './rule_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; import { RulesClient } from './rules_client'; @@ -74,9 +74,9 @@ export interface AlertServices< InstanceContext extends AlertInstanceContext = AlertInstanceContext, ActionGroupIds extends string = never > extends Services { - alertInstanceFactory: ( - id: string - ) => PublicAlertInstance; + alertFactory: { + create: (id: string) => PublicAlert; + }; shouldWriteAlerts: () => boolean; shouldStopExecution: () => boolean; search: IAbortableClusterClient; diff --git a/x-pack/plugins/apm/server/routes/alerts/register_error_count_alert_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/register_error_count_alert_type.test.ts index 2d98c09096f5e..01aa64b85f720 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_error_count_alert_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_error_count_alert_type.test.ts @@ -39,7 +39,7 @@ describe('Error count alert', () => { ); await executor({ params }); - expect(services.alertInstanceFactory).not.toBeCalled(); + expect(services.alertFactory.create).not.toBeCalled(); }); it('sends alerts with service name and environment for those that exceeded the threshold', async () => { @@ -138,7 +138,7 @@ describe('Error count alert', () => { 'apm.error_rate_foo_env-foo-2', 'apm.error_rate_bar_env-bar', ].forEach((instanceName) => - expect(services.alertInstanceFactory).toHaveBeenCalledWith(instanceName) + expect(services.alertFactory.create).toHaveBeenCalledWith(instanceName) ); expect(scheduleActions).toHaveBeenCalledTimes(3); diff --git a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts index 889fe3c16596e..41bb5126646fc 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts @@ -32,7 +32,7 @@ describe('Transaction duration anomaly alert', () => { services.scopedClusterClient.asCurrentUser.search ).not.toHaveBeenCalled(); - expect(services.alertInstanceFactory).not.toHaveBeenCalled(); + expect(services.alertFactory.create).not.toHaveBeenCalled(); }); it('ml jobs are not available', async () => { @@ -59,7 +59,7 @@ describe('Transaction duration anomaly alert', () => { services.scopedClusterClient.asCurrentUser.search ).not.toHaveBeenCalled(); - expect(services.alertInstanceFactory).not.toHaveBeenCalled(); + expect(services.alertFactory.create).not.toHaveBeenCalled(); }); it('anomaly is less than threshold', async () => { @@ -110,7 +110,7 @@ describe('Transaction duration anomaly alert', () => { expect( services.scopedClusterClient.asCurrentUser.search ).not.toHaveBeenCalled(); - expect(services.alertInstanceFactory).not.toHaveBeenCalled(); + expect(services.alertFactory.create).not.toHaveBeenCalled(); }); }); @@ -183,9 +183,9 @@ describe('Transaction duration anomaly alert', () => { await executor({ params }); - expect(services.alertInstanceFactory).toHaveBeenCalledTimes(1); + expect(services.alertFactory.create).toHaveBeenCalledTimes(1); - expect(services.alertInstanceFactory).toHaveBeenCalledWith( + expect(services.alertFactory.create).toHaveBeenCalledWith( 'apm.transaction_duration_anomaly_foo_development_type-foo' ); diff --git a/x-pack/plugins/apm/server/routes/alerts/register_transaction_error_rate_alert_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/register_transaction_error_rate_alert_type.test.ts index b0a99377c2989..64540e144d8a8 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_transaction_error_rate_alert_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_transaction_error_rate_alert_type.test.ts @@ -46,7 +46,7 @@ describe('Transaction error rate alert', () => { ); await executor({ params }); - expect(services.alertInstanceFactory).not.toBeCalled(); + expect(services.alertFactory.create).not.toBeCalled(); }); it('sends alerts for services that exceeded the threshold', async () => { @@ -117,12 +117,12 @@ describe('Transaction error rate alert', () => { await executor({ params }); - expect(services.alertInstanceFactory).toHaveBeenCalledTimes(1); + expect(services.alertFactory.create).toHaveBeenCalledTimes(1); - expect(services.alertInstanceFactory).toHaveBeenCalledWith( + expect(services.alertFactory.create).toHaveBeenCalledWith( 'apm.transaction_error_rate_foo_type-foo_env-foo' ); - expect(services.alertInstanceFactory).not.toHaveBeenCalledWith( + expect(services.alertFactory.create).not.toHaveBeenCalledWith( 'apm.transaction_error_rate_bar_type-bar_env-bar' ); diff --git a/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts b/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts index a8610bbcc8d37..f881b4476fe22 100644 --- a/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts +++ b/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts @@ -42,7 +42,7 @@ export const createRuleTypeMocks = () => { savedObjectsClient: { get: () => ({ attributes: { consumer: APM_SERVER_FEATURE_ID } }), }, - alertInstanceFactory: jest.fn(() => ({ scheduleActions })), + alertFactory: { create: jest.fn(() => ({ scheduleActions })) }, alertWithLifecycle: jest.fn(), logger: loggerMock, shouldWriteAlerts: () => true, diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 9301f17f4d99c..df79091612254 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -16,10 +16,7 @@ import { AlertInstanceState as AlertState, RecoveredActionGroup, } from '../../../../../alerting/common'; -import { - AlertInstance as Alert, - AlertTypeState as RuleTypeState, -} from '../../../../../alerting/server'; +import { Alert, AlertTypeState as RuleTypeState } from '../../../../../alerting/server'; import { AlertStates, InventoryMetricThresholdParams } from '../../../../common/alerting/metrics'; import { createFormatter } from '../../../../common/formatters'; import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_metric_label'; diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts index 90f9c508e1038..8f0809f581ad0 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts @@ -422,7 +422,7 @@ describe('Log threshold executor', () => { processUngroupedResults( results, ruleParams, - alertsMock.createAlertInstanceFactory, + alertsMock.createAlertFactory.create, alertUpdaterMock ); // First call, second argument @@ -486,7 +486,7 @@ describe('Log threshold executor', () => { processGroupByResults( results, ruleParams, - alertsMock.createAlertInstanceFactory, + alertsMock.createAlertFactory.create, alertUpdaterMock ); expect(alertUpdaterMock.mock.calls.length).toBe(2); diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index 73d7d1bf95363..5eedaac5f020a 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -16,7 +16,7 @@ import { ElasticsearchClient } from 'kibana/server'; import { ActionGroup, ActionGroupIdsOf, - AlertInstance as Alert, + Alert, AlertInstanceContext as AlertContext, AlertInstanceState as AlertState, AlertTypeState as RuleTypeState, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts index f762d694a59e7..0fb2ff87fd02c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts @@ -83,7 +83,7 @@ export const createMetricAnomalyExecutor = typical, influencers, } = first(data as MappedAnomalyHit[])!; - const alert = services.alertInstanceFactory(`${nodeType}-${metric}`); + const alert = services.alertFactory.create(`${nodeType}-${metric}`); alert.scheduleActions(FIRED_ACTIONS_ID, { alertState: stateToAlertMessage[AlertStates.ALERT], diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index b3c4de9658eda..57001d8cbdb1a 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -840,9 +840,9 @@ services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId }); const alertInstances = new Map(); -services.alertInstanceFactory.mockImplementation((instanceID: string) => { +services.alertFactory.create.mockImplementation((instanceID: string) => { const newAlertInstance: AlertTestInstance = { - instance: alertsMock.createAlertInstanceFactory(), + instance: alertsMock.createAlertFactory.create(), actionQueue: [], state: {}, }; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index f16b8a8135a37..9fbbe26fba126 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -15,10 +15,7 @@ import { AlertInstanceState as AlertState, RecoveredActionGroup, } from '../../../../../alerting/common'; -import { - AlertInstance as Alert, - AlertTypeState as RuleTypeState, -} from '../../../../../alerting/server'; +import { Alert, AlertTypeState as RuleTypeState } from '../../../../../alerting/server'; import { AlertStates, Comparator } from '../../../../common/alerting/metrics'; import { createFormatter } from '../../../../common/formatters'; import { InfraBackendLibs } from '../../infra_types'; diff --git a/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts b/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts index e30ea01b27cb5..68a86a927ac1a 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts @@ -139,7 +139,7 @@ export function registerAnomalyDetectionAlertType({ if (executionResult) { const alertInstanceName = executionResult.name; - const alertInstance = services.alertInstanceFactory(alertInstanceName); + const alertInstance = services.alertFactory.create(alertInstanceName); alertInstance.scheduleActions(ANOMALY_SCORE_MATCH_GROUP_ID, executionResult); } }, diff --git a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts index 5fd21d5372d23..1173f92930128 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts @@ -159,7 +159,7 @@ export function registerJobsMonitoringRuleType({ ); executionResult.forEach(({ name: alertInstanceName, context }) => { - const alertInstance = services.alertInstanceFactory(alertInstanceName); + const alertInstance = services.alertFactory.create(alertInstanceName); alertInstance.scheduleActions(ANOMALY_DETECTION_JOB_REALTIME_ISSUE, context); }); } diff --git a/x-pack/plugins/monitoring/server/alerts/base_rule.ts b/x-pack/plugins/monitoring/server/alerts/base_rule.ts index d13e6d9ed7f9b..0c48fed40ee34 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_rule.ts @@ -10,11 +10,16 @@ import { i18n } from '@kbn/i18n'; import { RuleType, AlertExecutorOptions, - AlertInstance, + Alert, RulesClient, AlertServices, } from '../../../alerting/server'; -import { Alert, AlertTypeParams, RawAlertInstance, SanitizedAlert } from '../../../alerting/common'; +import { + Alert as Rule, + AlertTypeParams, + RawAlertInstance, + SanitizedAlert, +} from '../../../alerting/common'; import { ActionsClient } from '../../../actions/server'; import { AlertState, @@ -121,7 +126,7 @@ export class BaseRule { }); if (existingRuleData.total > 0) { - return existingRuleData.data[0] as Alert; + return existingRuleData.data[0] as Rule; } const ruleActions = []; @@ -272,7 +277,7 @@ export class BaseRule { for (const node of nodes) { const newAlertStates: AlertNodeState[] = []; // quick fix for now so that non node level alerts will use the cluster id - const instance = services.alertInstanceFactory( + const instance = services.alertFactory.create( node.meta.nodeId || node.meta.instanceId || cluster.clusterUuid ); @@ -331,7 +336,7 @@ export class BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, instanceState: AlertInstanceState | AlertState | unknown, item: AlertData | unknown, cluster?: AlertCluster | unknown diff --git a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts index ed4ba69b8e254..d4f9284b40f8f 100644 --- a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts @@ -116,13 +116,15 @@ describe('CCRReadExceptionsRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts index 705d0c6b9c87f..e072602d6b711 100644 --- a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts @@ -21,7 +21,7 @@ import { CommonAlertFilter, CCRReadExceptionsStats, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_CCR_READ_EXCEPTIONS, RULE_DETAILS } from '../../common/constants'; import { fetchCCRReadExceptions } from '../lib/alerts/fetch_ccr_read_exceptions'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; @@ -209,7 +209,7 @@ export class CCRReadExceptionsRule extends BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts index 85030657825c4..ec4c15afe6731 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts @@ -81,13 +81,15 @@ describe('ClusterHealthRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts index b8810196c833a..a40fafc65d636 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts @@ -18,7 +18,7 @@ import { AlertClusterHealth, AlertInstanceState, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_CLUSTER_HEALTH, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertMessageTokenType, AlertClusterHealthType, AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; @@ -111,7 +111,7 @@ export class ClusterHealthRule extends BaseRule { } protected async executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts index bcd2c0cbb5810..cf2e0f29ddbc3 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts @@ -83,13 +83,15 @@ describe('CpuUsageRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts index fa4b64fd997c3..08a5cdb6c2780 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts @@ -22,7 +22,7 @@ import { CommonAlertParams, CommonAlertFilter, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_CPU_USAGE, RULE_DETAILS } from '../../common/constants'; // @ts-ignore import { ROUNDED_FLOAT } from '../../common/formatting'; @@ -145,7 +145,7 @@ export class CpuUsageRule extends BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts index daaded1c18c80..c08d32c395c1b 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts @@ -96,13 +96,15 @@ describe('DiskUsageRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts index 1e06f0649d107..a52a2fd79d654 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts @@ -22,7 +22,7 @@ import { AlertDiskUsageNodeStats, CommonAlertFilter, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_DISK_USAGE, RULE_DETAILS } from '../../common/constants'; // @ts-ignore import { ROUNDED_FLOAT } from '../../common/formatting'; @@ -152,7 +152,7 @@ export class DiskUsageRule extends BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts index 4531c5f0f1ffc..560ab805a236c 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts @@ -85,13 +85,15 @@ describe('ElasticsearchVersionMismatchAlert', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts index 9d89f827f9b10..43f5be14538b6 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts @@ -17,7 +17,7 @@ import { CommonAlertParams, AlertVersions, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_ELASTICSEARCH_VERSION_MISMATCH, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; @@ -87,7 +87,7 @@ export class ElasticsearchVersionMismatchRule extends BaseRule { } protected async executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts index b4444c9088073..b136d2c71f065 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts @@ -88,13 +88,15 @@ describe('KibanaVersionMismatchRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts index 24182c4a545d3..4e7a688b92ca9 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts @@ -17,7 +17,7 @@ import { CommonAlertParams, AlertVersions, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_KIBANA_VERSION_MISMATCH, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; @@ -97,7 +97,7 @@ export class KibanaVersionMismatchRule extends BaseRule { } protected async executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts index 0460064b4f7c5..a82b87cfe8a97 100644 --- a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts @@ -96,13 +96,15 @@ describe('LargeShardSizeRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts index 92be43b9c06c0..fbcf557a1f6f5 100644 --- a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts @@ -21,7 +21,7 @@ import { CommonAlertFilter, IndexShardSizeStats, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_LARGE_SHARD_SIZE, RULE_DETAILS } from '../../common/constants'; import { fetchIndexShardSize } from '../lib/alerts/fetch_index_shard_size'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; @@ -149,7 +149,7 @@ export class LargeShardSizeRule extends BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts index 86a6f666fcf87..0a69ee68ffeba 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts @@ -86,13 +86,15 @@ describe('LicenseExpirationRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts index 3a837a125a523..ad13ca9c56dfa 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts @@ -20,7 +20,7 @@ import { AlertLicense, AlertLicenseState, } from '../../common/types/alerts'; -import { AlertExecutorOptions, AlertInstance } from '../../../alerting/server'; +import { AlertExecutorOptions, Alert } from '../../../alerting/server'; import { RULE_LICENSE_EXPIRATION, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; @@ -143,7 +143,7 @@ export class LicenseExpirationRule extends BaseRule { } protected async executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts index 857a9bf5bfa79..b7790f81caa3e 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts @@ -86,13 +86,15 @@ describe('LogstashVersionMismatchRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts index ee3e5838d7d35..bca82de1a5fae 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts @@ -17,7 +17,7 @@ import { CommonAlertParams, AlertVersions, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_LOGSTASH_VERSION_MISMATCH, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; @@ -87,7 +87,7 @@ export class LogstashVersionMismatchRule extends BaseRule { } protected async executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts index 6e7aff2ae8fb4..785b1013304cd 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts @@ -83,13 +83,15 @@ describe('MemoryUsageRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts index 06ecf4bb450c8..62f790b1eb6d0 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts @@ -22,7 +22,7 @@ import { AlertMemoryUsageNodeStats, CommonAlertFilter, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_MEMORY_USAGE, RULE_DETAILS } from '../../common/constants'; // @ts-ignore import { ROUNDED_FLOAT } from '../../common/formatting'; @@ -158,7 +158,7 @@ export class MemoryUsageRule extends BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts index a8a96a61a4b25..b70bfe4bfb375 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts @@ -87,13 +87,15 @@ describe('MissingMonitoringDataRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts index fa7cbe009712a..9002855e2b67f 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts @@ -19,7 +19,7 @@ import { CommonAlertFilter, AlertNodeState, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_MISSING_MONITORING_DATA, RULE_DETAILS } from '../../common/constants'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { RawAlertInstance, SanitizedAlert } from '../../../alerting/common'; @@ -137,7 +137,7 @@ export class MissingMonitoringDataRule extends BaseRule { } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: { alertStates: AlertState[] }, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts index 3e24df3a6ef15..3704e909101e9 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts @@ -137,13 +137,15 @@ describe('NodesChangedAlert', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts index 82cf91e91b52a..3b14cf2428889 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts @@ -19,7 +19,7 @@ import { AlertInstanceState, AlertNodesChangedState, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { RULE_NODES_CHANGED, LEGACY_RULE_DETAILS } from '../../common/constants'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerting/common'; @@ -174,7 +174,7 @@ export class NodesChangedRule extends BaseRule { } protected async executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts index 0cca5eb81c95f..ca1b78a62646a 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts @@ -20,10 +20,10 @@ import { AlertState, AlertThreadPoolRejectionsStats, } from '../../common/types/alerts'; -import { AlertInstance } from '../../../alerting/server'; +import { Alert } from '../../../alerting/server'; import { fetchThreadPoolRejectionStats } from '../lib/alerts/fetch_thread_pool_rejections_stats'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; -import { Alert, RawAlertInstance } from '../../../alerting/common'; +import { Alert as Rule, RawAlertInstance } from '../../../alerting/common'; import { AlertingDefaults, createLink } from './alert_helpers'; import { Globals } from '../static_globals'; @@ -47,7 +47,7 @@ export class ThreadPoolRejectionsRuleBase extends BaseRule { } constructor( - sanitizedRule: Alert | undefined = undefined, + sanitizedRule: Rule | undefined = undefined, public readonly id: string, public readonly threadPoolType: string, public readonly name: string, @@ -176,7 +176,7 @@ export class ThreadPoolRejectionsRuleBase extends BaseRule { }; } protected executeActions( - instance: AlertInstance, + instance: Alert, { alertStates }: { alertStates: AlertState[] }, item: AlertData | null, cluster: AlertCluster diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts index 63a02088b9b65..45f8caacbcd41 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts @@ -89,13 +89,15 @@ describe('ThreadpoolSearchRejectionsRule', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts index da4c7ffaeffa0..47f6704eae70f 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts @@ -89,13 +89,15 @@ describe('ThreadpoolWriteRejectionsAlert', () => { const executorOptions = { services: { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), + alertFactory: { + create: jest.fn().mockImplementation(() => { + return { + replaceState, + scheduleActions, + getState, + }; + }), + }, }, state: {}, }; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts index 9ae3dff28b2ae..aea27787af080 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -13,7 +13,7 @@ import { v4 } from 'uuid'; import { difference } from 'lodash'; import { AlertExecutorOptions, - AlertInstance, + Alert, AlertInstanceContext, AlertInstanceState, AlertTypeParams, @@ -62,7 +62,7 @@ export type LifecycleAlertService< > = (alert: { id: string; fields: ExplicitAlertFields; -}) => AlertInstance; +}) => Alert; export interface LifecycleAlertServices< InstanceState extends AlertInstanceState = never, @@ -143,7 +143,7 @@ export const createLifecycleExecutor = > ): Promise> => { const { - services: { alertInstanceFactory, shouldWriteAlerts }, + services: { alertFactory, shouldWriteAlerts }, state: previousState, } = options; @@ -165,7 +165,7 @@ export const createLifecycleExecutor = > = { alertWithLifecycle: ({ id, fields }) => { currentAlerts[id] = fields; - return alertInstanceFactory(id); + return alertFactory.create(id); }, }; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index 3b9d8904c89b8..baa60664dea57 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -66,10 +66,12 @@ function createRule(shouldWriteAlerts: boolean = true) { const scheduleActions = jest.fn(); - const alertInstanceFactory = () => { - return { - scheduleActions, - } as any; + const alertFactory = { + create: () => { + return { + scheduleActions, + } as any; + }, }; return { @@ -107,7 +109,7 @@ function createRule(shouldWriteAlerts: boolean = true) { updatedBy: 'updatedBy', }, services: { - alertInstanceFactory, + alertFactory, savedObjectsClient: {} as any, scopedClusterClient: {} as any, shouldWriteAlerts: () => shouldWriteAlerts, diff --git a/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts b/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts index 37b4847bc9c69..5513aaf532522 100644 --- a/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts +++ b/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services_mock.ts @@ -34,5 +34,5 @@ export const createLifecycleAlertServicesMock = < >( alertServices: AlertServices ): LifecycleAlertServices => ({ - alertWithLifecycle: ({ id }) => alertServices.alertInstanceFactory(id), + alertWithLifecycle: ({ id }) => alertServices.alertFactory.create(id), }); diff --git a/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts b/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts index 08b1b0a8ecbf2..3d880988182b1 100644 --- a/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts +++ b/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts @@ -67,8 +67,7 @@ export const createDefaultAlertExecutorOptions = < params, spaceId: 'SPACE_ID', services: { - alertInstanceFactory: alertsMock.createAlertServices() - .alertInstanceFactory, + alertFactory: alertsMock.createAlertServices().alertFactory, savedObjectsClient: savedObjectsClientMock.create(), scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), shouldWriteAlerts: () => shouldWriteAlerts, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts index 1d97b7a39779a..ae253dfa3438c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.test.ts @@ -136,9 +136,9 @@ describe('legacyRules_notification_alert_type', () => { ); await alert.executor(payload); - expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + expect(alertServices.alertFactory.create).toHaveBeenCalled(); - const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + const [{ value: alertInstanceMock }] = alertServices.alertFactory.create.mock.results; expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( 'default', expect.objectContaining({ @@ -163,9 +163,9 @@ describe('legacyRules_notification_alert_type', () => { ) ); await alert.executor(payload); - expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + expect(alertServices.alertFactory.create).toHaveBeenCalled(); - const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + const [{ value: alertInstanceMock }] = alertServices.alertFactory.create.mock.results; expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( 'default', expect.objectContaining({ @@ -192,9 +192,9 @@ describe('legacyRules_notification_alert_type', () => { ) ); await alert.executor(payload); - expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + expect(alertServices.alertFactory.create).toHaveBeenCalled(); - const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + const [{ value: alertInstanceMock }] = alertServices.alertFactory.create.mock.results; expect(alertInstanceMock.scheduleActions).toHaveBeenCalledWith( 'default', expect.objectContaining({ @@ -204,7 +204,7 @@ describe('legacyRules_notification_alert_type', () => { ); }); - it('should not call alertInstanceFactory if signalsCount was 0', async () => { + it('should not call alertFactory.create if signalsCount was 0', async () => { const ruleAlert = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); alertServices.savedObjectsClient.get.mockResolvedValue({ id: 'id', @@ -218,7 +218,7 @@ describe('legacyRules_notification_alert_type', () => { await alert.executor(payload); - expect(alertServices.alertInstanceFactory).not.toHaveBeenCalled(); + expect(alertServices.alertFactory.create).not.toHaveBeenCalled(); }); it('should call scheduleActions if signalsCount was greater than 0', async () => { @@ -237,9 +237,9 @@ describe('legacyRules_notification_alert_type', () => { await alert.executor(payload); - expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); + expect(alertServices.alertFactory.create).toHaveBeenCalled(); - const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + const [{ value: alertInstanceMock }] = alertServices.alertFactory.create.mock.results; expect(alertInstanceMock.replaceState).toHaveBeenCalledWith( expect.objectContaining({ signals_count: 100 }) ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts index 6a5a9478681f3..62d187bd3ea0d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/legacy_rules_notification_alert_type.ts @@ -119,7 +119,7 @@ export const legacyRulesNotificationAlertType = ({ ); if (signalsCount !== 0) { - const alertInstance = services.alertInstanceFactory(alertId); + const alertInstance = services.alertFactory.create(alertId); scheduleNotificationActions({ alertInstance, signalsCount, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.test.ts index b40f6c6f8a72d..eebda81fd63f0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.test.ts @@ -54,7 +54,7 @@ describe('schedule_notification_actions', () => { }; it('Should schedule actions with unflatted and legacy context', () => { - const alertInstance = alertServices.alertInstanceFactory(alertId); + const alertInstance = alertServices.alertFactory.create(alertId); const signals = [sampleThresholdAlert._source, sampleThresholdAlert._source]; scheduleNotificationActions({ alertInstance, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts index 9b20b031eea0f..394e431203a24 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_notification_actions.ts @@ -6,7 +6,7 @@ */ import { mapKeys, snakeCase } from 'lodash/fp'; -import { AlertInstance } from '../../../../../alerting/server'; +import { Alert } from '../../../../../alerting/server'; import { expandDottedObject } from '../../../../common/utils/expand_dotted'; import { RuleParams } from '../schemas/rule_schemas'; import aadFieldConversion from '../routes/index/signal_aad_mapping.json'; @@ -46,7 +46,7 @@ const formatAlertsForNotificationActions = (alerts: unknown[]): unknown[] => { }; interface ScheduleNotificationActions { - alertInstance: AlertInstance; + alertInstance: Alert; signalsCount: number; resultsLink: string; ruleParams: NotificationRuleTypeParams; @@ -59,7 +59,7 @@ export const scheduleNotificationActions = ({ resultsLink = '', ruleParams, signals, -}: ScheduleNotificationActions): AlertInstance => +}: ScheduleNotificationActions): Alert => alertInstance .replaceState({ signals_count: signalsCount, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts index 964df3c91eb08..b5dffa7b34c14 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts @@ -82,7 +82,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [], @@ -107,7 +107,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [ @@ -137,7 +137,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [], @@ -166,7 +166,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [], @@ -197,7 +197,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [], @@ -235,7 +235,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [], @@ -271,7 +271,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [], @@ -313,7 +313,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [ @@ -375,7 +375,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [ @@ -435,7 +435,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [ @@ -497,7 +497,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [ @@ -559,7 +559,7 @@ describe('schedule_throttle_notification_actions', () => { }, }) ), - alertInstance: alertsMock.createAlertInstanceFactory(), + alertInstance: alertsMock.createAlertFactory.create(), notificationRuleParams, logger, signals: [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts index cab590b3e2513..2399962ad281e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts @@ -7,7 +7,7 @@ import { ElasticsearchClient, SavedObject, Logger } from 'src/core/server'; import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; -import { AlertInstance } from '../../../../../alerting/server'; +import { Alert } from '../../../../../alerting/server'; import { RuleParams } from '../schemas/rule_schemas'; import { deconflictSignalsAndResults, getNotificationResultsLink } from '../notifications/utils'; import { DEFAULT_RULE_NOTIFICATION_QUERY_SIZE } from '../../../../common/constants'; @@ -26,7 +26,7 @@ interface ScheduleThrottledNotificationActionsOptions { outputIndex: RuleParams['outputIndex']; ruleId: RuleParams['ruleId']; esClient: ElasticsearchClient; - alertInstance: AlertInstance; + alertInstance: Alert; notificationRuleParams: NotificationRuleTypeParams; signals: unknown[]; logger: Logger; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts index ed0aa04b6a08c..8d89bc66b2041 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts @@ -34,7 +34,7 @@ import { } from '../../../../../../alerting/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ExecutorType } from '../../../../../../alerting/server/types'; -import { AlertInstance } from '../../../../../../alerting/server'; +import { Alert } from '../../../../../../alerting/server'; import { ConfigType } from '../../../../config'; import { alertInstanceFactoryStub } from '../../signals/preview/alert_instance_factory_stub'; import { CreateRuleOptions, CreateSecurityRuleTypeWrapperProps } from '../../rule_types/types'; @@ -140,12 +140,14 @@ export const previewRulesRoute = async ( ruleTypeName: string, params: TParams, shouldWriteAlerts: () => boolean, - alertInstanceFactory: ( - id: string - ) => Pick< - AlertInstance, - 'getState' | 'replaceState' | 'scheduleActions' | 'scheduleActionsWithSubGroup' - > + alertFactory: { + create: ( + id: string + ) => Pick< + Alert, + 'getState' | 'replaceState' | 'scheduleActions' | 'scheduleActionsWithSubGroup' + >; + } ) => { let statePreview = runState as TState; @@ -178,7 +180,7 @@ export const previewRulesRoute = async ( services: { shouldWriteAlerts, shouldStopExecution: () => false, - alertInstanceFactory, + alertFactory, // Just use es client always for preview search: context.core.elasticsearch.client, savedObjectsClient: context.core.savedObjects.client, @@ -223,7 +225,7 @@ export const previewRulesRoute = async ( queryAlertType.name, previewRuleParams, () => true, - alertInstanceFactoryStub + { create: alertInstanceFactoryStub } ); break; case 'threshold': @@ -236,7 +238,7 @@ export const previewRulesRoute = async ( thresholdAlertType.name, previewRuleParams, () => true, - alertInstanceFactoryStub + { create: alertInstanceFactoryStub } ); break; case 'threat_match': @@ -249,7 +251,7 @@ export const previewRulesRoute = async ( threatMatchAlertType.name, previewRuleParams, () => true, - alertInstanceFactoryStub + { create: alertInstanceFactoryStub } ); break; case 'eql': @@ -260,7 +262,7 @@ export const previewRulesRoute = async ( eqlAlertType.name, previewRuleParams, () => true, - alertInstanceFactoryStub + { create: alertInstanceFactoryStub } ); break; case 'machine_learning': @@ -271,7 +273,7 @@ export const previewRulesRoute = async ( mlAlertType.name, previewRuleParams, () => true, - alertInstanceFactoryStub + { create: alertInstanceFactoryStub } ); break; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts index 3774930204ae6..3d96e3bb77907 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts @@ -76,7 +76,7 @@ export const createRuleTypeMocks = ( search: elasticsearchServiceMock.createScopedClusterClient(), savedObjectsClient: mockSavedObjectsClient, scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertInstanceFactory: jest.fn(() => ({ scheduleActions })), + alertFactory: { create: jest.fn(() => ({ scheduleActions })) }, findAlerts: jest.fn(), // TODO: does this stay? alertWithPersistence: jest.fn(), logger: loggerMock, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index e2fc5442d4c80..00244832d0191 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -315,7 +315,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = if (completeRule.ruleConfig.throttle != null) { await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), + alertInstance: services.alertFactory.create(alertId), throttle: completeRule.ruleConfig.throttle ?? '', startedAt, id: alertId, @@ -329,7 +329,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = logger, }); } else if (createdSignalsCount) { - const alertInstance = services.alertInstanceFactory(alertId); + const alertInstance = services.alertFactory.create(alertId); scheduleNotificationActions({ alertInstance, signalsCount: createdSignalsCount, @@ -371,7 +371,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early if (completeRule.ruleConfig.throttle != null) { await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), + alertInstance: services.alertFactory.create(alertId), throttle: completeRule.ruleConfig.throttle ?? '', startedAt, id: completeRule.alertId, @@ -403,7 +403,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early if (completeRule.ruleConfig.throttle != null) { await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), + alertInstance: services.alertFactory.create(alertId), throttle: completeRule.ruleConfig.throttle ?? '', startedAt, id: completeRule.alertId, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/alert_instance_factory_stub.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/alert_instance_factory_stub.ts index d09314312c78d..7cc709bbe8994 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/alert_instance_factory_stub.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/alert_instance_factory_stub.ts @@ -12,7 +12,7 @@ import { AlertTypeState, } from '../../../../../../alerting/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertInstance } from '../../../../../../alerting/server/alert_instance'; +import { Alert } from '../../../../../../alerting/server/alert'; export const alertInstanceFactoryStub = < TParams extends RuleParams, @@ -27,13 +27,13 @@ export const alertInstanceFactoryStub = < return {} as unknown as TInstanceState; }, replaceState(state: TInstanceState) { - return new AlertInstance({ + return new Alert({ state: {} as TInstanceState, meta: { lastScheduledActions: { group: 'default', date: new Date() } }, }); }, scheduleActions(actionGroup: TActionGroupIds, alertcontext: TInstanceContext) { - return new AlertInstance({ + return new Alert({ state: {} as TInstanceState, meta: { lastScheduledActions: { group: 'default', date: new Date() } }, }); @@ -43,7 +43,7 @@ export const alertInstanceFactoryStub = < subgroup: string, alertcontext: TInstanceContext ) { - return new AlertInstance({ + return new Alert({ state: {} as TInstanceState, meta: { lastScheduledActions: { group: 'default', date: new Date() } }, }); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts index c0d05c44201fb..307496e2be391 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts @@ -178,7 +178,7 @@ describe('alertType', () => { }, }); - expect(alertServices.alertInstanceFactory).not.toHaveBeenCalled(); + expect(alertServices.alertFactory.create).not.toHaveBeenCalled(); expect(result).toMatchInlineSnapshot(` Object { @@ -257,8 +257,8 @@ describe('alertType', () => { }, }); - expect(alertServices.alertInstanceFactory).toHaveBeenCalledWith(ConditionMetAlertInstanceId); - const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + expect(alertServices.alertFactory.create).toHaveBeenCalledWith(ConditionMetAlertInstanceId); + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ latestTimestamp: undefined, dateStart: expect.any(String), @@ -328,7 +328,7 @@ describe('alertType', () => { }, }); - const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ // ensure the invalid "latestTimestamp" in the state is stored as an ISO string going forward latestTimestamp: new Date(previousTimestamp).toISOString(), @@ -410,7 +410,7 @@ describe('alertType', () => { }, }); - const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ latestTimestamp: undefined, dateStart: expect.any(String), @@ -488,7 +488,7 @@ describe('alertType', () => { }; const result = await alertType.executor(executorOptions); - const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ latestTimestamp: undefined, dateStart: expect.any(String), @@ -518,7 +518,7 @@ describe('alertType', () => { state: result as EsQueryAlertState, }); const existingInstance: AlertInstanceMock = - alertServices.alertInstanceFactory.mock.results[1].value; + alertServices.alertFactory.create.mock.results[1].value; expect(existingInstance.replaceState).toHaveBeenCalledWith({ latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(), dateStart: expect.any(String), @@ -601,7 +601,7 @@ describe('alertType', () => { }, }); - const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ latestTimestamp: undefined, dateStart: expect.any(String), @@ -685,7 +685,7 @@ describe('alertType', () => { }, }); - const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; expect(instance.replaceState).toHaveBeenCalledWith({ latestTimestamp: undefined, dateStart: expect.any(String), diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts index 9dca9e9c3fc61..6a1fcc6a3d7bb 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts @@ -160,7 +160,7 @@ export function getAlertType(logger: Logger): RuleType< > ) { const { alertId, name, services, params, state } = options; - const { alertInstanceFactory, search } = services; + const { alertFactory, search } = services; const previousTimestamp = state.latestTimestamp; const abortableEsClient = search.asCurrentUser; @@ -255,7 +255,7 @@ export function getAlertType(logger: Logger): RuleType< }; const actionContext = addMessages(options, baseContext, params); - const alertInstance = alertInstanceFactory(ConditionMetAlertInstanceId); + const alertInstance = alertFactory.create(ConditionMetAlertInstanceId); alertInstance // store the params we would need to recreate the query that led to this alert instance .replaceState({ latestTimestamp: timestamp, dateStart, dateEnd }) diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts index ecd08d3dc432f..aca79e29cd3e5 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts @@ -87,11 +87,11 @@ export function transformResults( export function getActiveEntriesAndGenerateAlerts( prevLocationMap: Map, currLocationMap: Map, - alertInstanceFactory: AlertServices< + alertFactory: AlertServices< GeoContainmentInstanceState, GeoContainmentInstanceContext, typeof ActionGroupId - >['alertInstanceFactory'], + >['alertFactory'], shapesIdsNamesMap: Record, currIntervalEndTime: Date ) { @@ -113,7 +113,7 @@ export function getActiveEntriesAndGenerateAlerts( }; const alertInstanceId = `${entityName}-${context.containingBoundaryName}`; if (shapeLocationId !== OTHER_CATEGORY) { - alertInstanceFactory(alertInstanceId).scheduleActions(ActionGroupId, context); + alertFactory.create(alertInstanceId).scheduleActions(ActionGroupId, context); } }); @@ -189,7 +189,7 @@ export const getGeoContainmentExecutor = (log: Logger): GeoContainmentAlertType[ const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( prevLocationMap, currLocationMap, - services.alertInstanceFactory, + services.alertFactory, shapesIdsNamesMap, currIntervalEndTime ); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts index 8b78441d174b2..dc633e298490c 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts @@ -20,9 +20,9 @@ import { OTHER_CATEGORY } from '../es_query_builder'; import { GeoContainmentInstanceContext, GeoContainmentInstanceState } from '../alert_type'; import type { GeoContainmentParams } from '../alert_type'; -const alertInstanceFactory = - (contextKeys: unknown[], testAlertActionArr: unknown[]) => (instanceId: string) => { - const alertInstance = alertsMock.createAlertInstanceFactory< +const alertFactory = (contextKeys: unknown[], testAlertActionArr: unknown[]) => ({ + create: (instanceId: string) => { + const alertInstance = alertsMock.createAlertFactory.create< GeoContainmentInstanceState, GeoContainmentInstanceContext >(); @@ -39,7 +39,8 @@ const alertInstanceFactory = } ); return alertInstance; - }; + }, +}); describe('geo_containment', () => { describe('transformResults', () => { @@ -253,7 +254,7 @@ describe('geo_containment', () => { const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( emptyPrevLocationMap, currLocationMap, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -278,7 +279,7 @@ describe('geo_containment', () => { const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( prevLocationMapWithIdenticalEntityEntry, currLocationMap, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -317,7 +318,7 @@ describe('geo_containment', () => { const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( prevLocationMapWithNonIdenticalEntityEntry, currLocationMap, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -340,7 +341,7 @@ describe('geo_containment', () => { const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( emptyPrevLocationMap, currLocationMapWithOther, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -373,7 +374,7 @@ describe('geo_containment', () => { getActiveEntriesAndGenerateAlerts( emptyPrevLocationMap, currLocationMapWithThreeMore, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -410,7 +411,7 @@ describe('geo_containment', () => { const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( emptyPrevLocationMap, currLocationMapWithOther, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -442,7 +443,7 @@ describe('geo_containment', () => { const allActiveEntriesMap = getActiveEntriesAndGenerateAlerts( emptyPrevLocationMap, currLocationMapWithOther, - alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory(contextKeys, testAlertActionArr), emptyShapesIdsNamesMap, currentDateTime ); @@ -514,7 +515,7 @@ describe('geo_containment', () => { const alertServicesWithSearchMock: AlertServicesMock = { ...alertsMock.createAlertServices(), // @ts-ignore - alertInstanceFactory: alertInstanceFactory(contextKeys, testAlertActionArr), + alertFactory: alertFactory(contextKeys, testAlertActionArr), scopedClusterClient: { asCurrentUser: { // @ts-ignore @@ -538,6 +539,7 @@ describe('geo_containment', () => { it('should query for shapes if state does not contain shapes', async () => { const executor = await getGeoContainmentExecutor(mockLogger); + // @ts-ignore const executionResult = await executor({ previousStartedAt, startedAt, @@ -557,6 +559,7 @@ describe('geo_containment', () => { it('should not query for shapes if state contains shapes', async () => { const executor = await getGeoContainmentExecutor(mockLogger); + // @ts-ignore const executionResult = await executor({ previousStartedAt, startedAt, @@ -575,6 +578,7 @@ describe('geo_containment', () => { it('should carry through shapes filters in state to next call unmodified', async () => { const executor = await getGeoContainmentExecutor(mockLogger); + // @ts-ignore const executionResult = await executor({ previousStartedAt, startedAt, @@ -610,6 +614,7 @@ describe('geo_containment', () => { ], }; const executor = await getGeoContainmentExecutor(mockLogger); + // @ts-ignore const executionResult = await executor({ previousStartedAt, startedAt, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts index f6b1c4a3a3b0a..e55ce6e3a3aba 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts @@ -203,7 +203,7 @@ describe('alertType', () => { }, }); - expect(alertServices.alertInstanceFactory).toHaveBeenCalledWith('all documents'); + expect(alertServices.alertFactory.create).toHaveBeenCalledWith('all documents'); }); it('should ensure a null result does not fire actions', async () => { @@ -269,7 +269,7 @@ describe('alertType', () => { }, }); - expect(customAlertServices.alertInstanceFactory).not.toHaveBeenCalled(); + expect(customAlertServices.alertFactory.create).not.toHaveBeenCalled(); }); it('should ensure an undefined result does not fire actions', async () => { @@ -335,6 +335,6 @@ describe('alertType', () => { }, }); - expect(customAlertServices.alertInstanceFactory).not.toHaveBeenCalled(); + expect(customAlertServices.alertFactory.create).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts index e31744e770462..0eb2810626ac3 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts @@ -134,7 +134,7 @@ export function getAlertType( options: AlertExecutorOptions ) { const { alertId, name, services, params } = options; - const { alertInstanceFactory, search } = services; + const { alertFactory, search } = services; const compareFn = ComparatorFns.get(params.thresholdComparator); if (compareFn == null) { @@ -208,7 +208,7 @@ export function getAlertType( conditions: humanFn, }; const actionContext = addMessages(options, baseContext, params); - const alertInstance = alertInstanceFactory(instanceId); + const alertInstance = alertFactory.create(instanceId); alertInstance.scheduleActions(ActionGroupId, actionContext); logger.debug(`scheduled actionGroup: ${JSON.stringify(actionContext)}`); } diff --git a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts index f33afc2b54285..8b9897a0e9436 100644 --- a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts +++ b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts @@ -100,7 +100,7 @@ export function getTransformHealthRuleType(): RuleType< isExportable: true, async executor(options) { const { - services: { scopedClusterClient, alertInstanceFactory }, + services: { scopedClusterClient, alertFactory }, params, } = options; @@ -112,7 +112,7 @@ export function getTransformHealthRuleType(): RuleType< if (executionResult.length > 0) { executionResult.forEach(({ name: alertInstanceName, context }) => { - const alertInstance = alertInstanceFactory(alertInstanceName); + const alertInstance = alertFactory.create(alertInstanceName); alertInstance.scheduleActions(TRANSFORM_ISSUE, context); }); } diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts b/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts index 8236af03de85c..e662160bef3e0 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts @@ -16,7 +16,7 @@ import { commonStateTranslations, tlsTranslations } from './translations'; import { ActionGroupIdsOf } from '../../../../alerting/common'; import { AlertInstanceContext } from '../../../../alerting/common'; -import { AlertInstance } from '../../../../alerting/server'; +import { Alert } from '../../../../alerting/server'; import { savedObjectsAdapter } from '../saved_objects/saved_objects'; import { createUptimeESClient } from '../lib'; @@ -28,7 +28,7 @@ import { export type ActionGroupIds = ActionGroupIdsOf; -type TLSAlertInstance = AlertInstance, AlertInstanceContext, ActionGroupIds>; +type TLSAlertInstance = Alert, AlertInstanceContext, ActionGroupIds>; interface TlsAlertState { count: number; @@ -113,10 +113,7 @@ export const tlsLegacyAlertFactory: UptimeAlertTypeFactory = (_s }, isExportable: true, minimumLicenseRequired: 'basic', - async executor({ - services: { alertInstanceFactory, scopedClusterClient, savedObjectsClient }, - state, - }) { + async executor({ services: { alertFactory, scopedClusterClient, savedObjectsClient }, state }) { const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient); const uptimeEsClient = createUptimeESClient({ @@ -156,7 +153,7 @@ export const tlsLegacyAlertFactory: UptimeAlertTypeFactory = (_s 'd' ) .valueOf(); - const alertInstance: TLSAlertInstance = alertInstanceFactory(TLS_LEGACY.id); + const alertInstance: TLSAlertInstance = alertFactory.create(TLS_LEGACY.id); const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold); alertInstance.replaceState({ ...updateState(state, foundCerts), diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts index a99de22181766..b1ad23170ae07 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts @@ -122,7 +122,7 @@ async function alwaysFiringExecutor(alertExecutorOptions: any) { } if (group) { - const instance = services.alertInstanceFactory('1').replaceState({ instanceStateValue: true }); + const instance = services.alertFactory.create('1').replaceState({ instanceStateValue: true }); if (subgroup) { instance.scheduleActionsWithSubGroup(group, subgroup, { @@ -177,8 +177,8 @@ function getCumulativeFiringAlertType() { const runCount = (state.runCount || 0) + 1; times(runCount, (index) => { - services - .alertInstanceFactory(`instance-${index}`) + services.alertFactory + .create(`instance-${index}`) .replaceState({ instanceStateValue: true }) .scheduleActions(group); }); @@ -446,13 +446,13 @@ function getPatternFiringAlertType() { for (const [instanceId, instancePattern] of Object.entries(pattern)) { const scheduleByPattern = instancePattern[patternIndex]; if (scheduleByPattern === true) { - services.alertInstanceFactory(instanceId).scheduleActions('default', { + services.alertFactory.create(instanceId).scheduleActions('default', { ...EscapableStrings, deep: DeepContextVariables, }); } else if (typeof scheduleByPattern === 'string') { - services - .alertInstanceFactory(instanceId) + services.alertFactory + .create(instanceId) .scheduleActionsWithSubGroup('default', scheduleByPattern); } } @@ -538,7 +538,7 @@ function getLongRunningPatternRuleType(cancelAlertsOnRuleTimeout: boolean = true return {}; } - services.alertInstanceFactory('alert').scheduleActions('default', {}); + services.alertFactory.create('alert').scheduleActions('default', {}); // run long if pattern says to if (pattern[globalPatternIndex++] === true) { diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts index de0fb1829c2b1..2cd6b50a2062f 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts @@ -54,8 +54,8 @@ export const alwaysFiringAlertType: RuleType< const { services, state, params } = alertExecutorOptions; (params.instances || []).forEach((instance: { id: string; state: any }) => { - services - .alertInstanceFactory(instance.id) + services.alertFactory + .create(instance.id) .replaceState({ instanceStateValue: true, ...(instance.state || {}) }) .scheduleActions('default'); }); diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts index ebef251984cd6..89f15705beb59 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts @@ -177,7 +177,7 @@ export default function createLifecycleExecutorApiTest({ getService }: FtrProvid producer: 'observability.test', }, services: { - alertInstanceFactory: sinon.stub(), + alertFactory: { create: sinon.stub() }, shouldWriteAlerts: sinon.stub().returns(true), }, } as unknown as RuleExecutorOptions< From 8dffa53561730448f263d949f312330813d375b7 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Mon, 7 Feb 2022 17:24:08 -0500 Subject: [PATCH 05/44] [Dashboard] Remove Deprecated References (#121925) Remove index pattern references. Replace data filter imports with imports from es-query package --- src/plugins/dashboard/kibana.json | 1 + .../public/application/dashboard_router.tsx | 4 +- .../hooks/use_dashboard_app_state.test.tsx | 16 ++--- .../hooks/use_dashboard_app_state.ts | 19 +++--- .../lib/dashboard_control_group.ts | 2 +- .../application/lib/diff_dashboard_state.ts | 2 +- .../dashboard/public/application/lib/index.ts | 2 +- .../lib/load_saved_dashboard_state.ts | 4 +- .../public/application/lib/save_dashboard.ts | 7 +-- .../lib/sync_dashboard_container_input.ts | 11 +--- ...tterns.ts => sync_dashboard_data_views.ts} | 59 ++++++++++--------- .../get_dashboard_list_item_link.test.ts | 4 +- .../test_helpers/make_default_services.ts | 4 +- .../application/top_nav/dashboard_top_nav.tsx | 2 +- src/plugins/dashboard/public/locator.test.ts | 4 +- src/plugins/dashboard/public/locator.ts | 8 ++- src/plugins/dashboard/public/plugin.tsx | 13 ++-- .../dashboard/public/services/data_views.ts | 9 +++ src/plugins/dashboard/public/types.ts | 17 +++--- .../dashboard/public/url_generator.test.ts | 4 +- .../saved_objects/dashboard_migrations.ts | 6 +- .../move_filters_to_query.test.ts | 6 +- .../replace_index_pattern_reference.ts | 4 +- src/plugins/dashboard/tsconfig.json | 1 + 24 files changed, 113 insertions(+), 96 deletions(-) rename src/plugins/dashboard/public/application/lib/{sync_dashboard_index_patterns.ts => sync_dashboard_data_views.ts} (56%) create mode 100644 src/plugins/dashboard/public/services/data_views.ts diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index 683a1a551f81d..0130d4a5f8118 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -8,6 +8,7 @@ "version": "kibana", "requiredPlugins": [ "data", + "dataViews", "embeddable", "controls", "inspector", diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx index ae16527b64440..05d663bdac265 100644 --- a/src/plugins/dashboard/public/application/dashboard_router.tsx +++ b/src/plugins/dashboard/public/application/dashboard_router.tsx @@ -110,7 +110,7 @@ export async function mountApp({ uiSettings: coreStart.uiSettings, scopedHistory: () => scopedHistory, screenshotModeService: screenshotMode, - indexPatterns: dataStart.indexPatterns, + dataViews: dataStart.dataViews, savedQueryService: dataStart.query.savedQueries, savedObjectsClient: coreStart.savedObjects.client, savedDashboards: dashboardStart.getSavedDashboardLoader(), @@ -212,7 +212,7 @@ export async function mountApp({ .getIncomingEmbeddablePackage(DashboardConstants.DASHBOARDS_ID, false) ); if (!hasEmbeddableIncoming) { - dataStart.indexPatterns.clearCache(); + dataStart.dataViews.clearCache(); } // dispatch synthetic hash change event to update hash history objects diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx index 0ef21fca26f29..039a600d153b2 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.test.tsx @@ -24,13 +24,15 @@ import { EmbeddableFactory, ViewMode } from '../../services/embeddable'; import { dashboardStateStore, setDescription, setViewMode } from '../state'; import { DashboardContainerServices } from '../embeddable/dashboard_container'; import { createKbnUrlStateStorage, defer } from '../../../../kibana_utils/public'; -import { Filter, IIndexPattern, IndexPatternsContract } from '../../services/data'; import { useDashboardAppState, UseDashboardStateProps } from './use_dashboard_app_state'; import { getSampleDashboardInput, getSavedDashboardMock, makeDefaultServices, } from '../test_helpers'; +import { DataViewsContract } from '../../services/data'; +import { DataView } from '../../services/data_views'; +import type { Filter } from '@kbn/es-query'; interface SetupEmbeddableFactoryReturn { finalizeEmbeddableCreation: () => void; @@ -56,12 +58,10 @@ const createDashboardAppStateProps = (): UseDashboardStateProps => ({ const createDashboardAppStateServices = () => { const defaults = makeDefaultServices(); - const indexPatterns = {} as IndexPatternsContract; - const defaultIndexPattern = { id: 'foo', fields: [{ name: 'bar' }] } as IIndexPattern; - indexPatterns.ensureDefaultDataView = jest.fn().mockImplementation(() => Promise.resolve(true)); - indexPatterns.getDefault = jest - .fn() - .mockImplementation(() => Promise.resolve(defaultIndexPattern)); + const dataViews = {} as DataViewsContract; + const defaultDataView = { id: 'foo', fields: [{ name: 'bar' }] } as DataView; + dataViews.ensureDefaultDataView = jest.fn().mockImplementation(() => Promise.resolve(true)); + dataViews.getDefault = jest.fn().mockImplementation(() => Promise.resolve(defaultDataView)); const data = dataPluginMock.createStartContract(); data.query.filterManager.getUpdates$ = jest.fn().mockImplementation(() => of(void 0)); @@ -71,7 +71,7 @@ const createDashboardAppStateServices = () => { .fn() .mockImplementation(() => of(void 0)); - return { ...defaults, indexPatterns, data }; + return { ...defaults, dataViews, data }; }; const setupEmbeddableFactory = ( diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts index 8c58eab0ded83..2ce1c87252d38 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts @@ -15,6 +15,7 @@ import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs'; import { DashboardConstants } from '../..'; import { ViewMode } from '../../services/embeddable'; import { useKibana } from '../../services/kibana_react'; +import { DataView } from '../../services/data_views'; import { getNewDashboardTitle } from '../../dashboard_strings'; import { IKbnUrlStateStorage } from '../../services/kibana_utils'; import { setDashboardState, useDashboardDispatch, useDashboardSelector } from '../state'; @@ -30,7 +31,7 @@ import { tryDestroyDashboardContainer, syncDashboardContainerInput, savedObjectToDashboardState, - syncDashboardIndexPatterns, + syncDashboardDataViews, syncDashboardFilterState, loadSavedDashboardState, buildDashboardContainer, @@ -81,7 +82,7 @@ export const useDashboardAppState = ({ core, chrome, embeddable, - indexPatterns, + dataViews, usageCollection, savedDashboards, initializerContext, @@ -121,7 +122,7 @@ export const useDashboardAppState = ({ search, history, embeddable, - indexPatterns, + dataViews, notifications, kibanaVersion, savedDashboards, @@ -234,11 +235,11 @@ export const useDashboardAppState = ({ /** * Start syncing index patterns between the Query Service and the Dashboard Container. */ - const indexPatternsSubscription = syncDashboardIndexPatterns({ + const dataViewsSubscription = syncDashboardDataViews({ dashboardContainer, - indexPatterns: dashboardBuildContext.indexPatterns, - onUpdateIndexPatterns: (newIndexPatterns) => - setDashboardAppState((s) => ({ ...s, indexPatterns: newIndexPatterns })), + dataViews: dashboardBuildContext.dataViews, + onUpdateDataViews: (newDataViews: DataView[]) => + setDashboardAppState((s) => ({ ...s, dataViews: newDataViews })), }); /** @@ -339,7 +340,7 @@ export const useDashboardAppState = ({ stopWatchingAppStateInUrl(); stopSyncingDashboardFilterState(); lastSavedSubscription.unsubscribe(); - indexPatternsSubscription.unsubscribe(); + dataViewsSubscription.unsubscribe(); tryDestroyDashboardContainer(dashboardContainer); setDashboardAppState((state) => ({ ...state, @@ -368,7 +369,7 @@ export const useDashboardAppState = ({ usageCollection, scopedHistory, notifications, - indexPatterns, + dataViews, kibanaVersion, embeddable, docTitle, diff --git a/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts b/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts index 90d5a67c3da47..0d1eb3537377f 100644 --- a/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts +++ b/src/plugins/dashboard/public/application/lib/dashboard_control_group.ts @@ -8,7 +8,7 @@ import { Subscription } from 'rxjs'; import deepEqual from 'fast-deep-equal'; -import { compareFilters, COMPARE_ALL_OPTIONS, Filter } from '@kbn/es-query'; +import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query'; import { distinctUntilChanged, distinctUntilKeyChanged } from 'rxjs/operators'; import { DashboardContainer } from '..'; diff --git a/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts index 264c8fcb1de2e..729b0d06f4ab8 100644 --- a/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts @@ -8,7 +8,7 @@ import { xor, omit, isEmpty } from 'lodash'; import fastIsEqual from 'fast-deep-equal'; -import { compareFilters, COMPARE_ALL_OPTIONS, Filter, isFilterPinned } from '@kbn/es-query'; +import { compareFilters, COMPARE_ALL_OPTIONS, type Filter, isFilterPinned } from '@kbn/es-query'; import { DashboardContainerInput } from '../..'; import { controlGroupInputIsEqual } from './dashboard_control_group'; diff --git a/src/plugins/dashboard/public/application/lib/index.ts b/src/plugins/dashboard/public/application/lib/index.ts index 58f962591b67c..eab3604ff841b 100644 --- a/src/plugins/dashboard/public/application/lib/index.ts +++ b/src/plugins/dashboard/public/application/lib/index.ts @@ -18,7 +18,7 @@ export { DashboardSessionStorage } from './dashboard_session_storage'; export { loadSavedDashboardState } from './load_saved_dashboard_state'; export { attemptLoadDashboardByTitle } from './load_dashboard_by_title'; export { syncDashboardFilterState } from './sync_dashboard_filter_state'; -export { syncDashboardIndexPatterns } from './sync_dashboard_index_patterns'; +export { syncDashboardDataViews } from './sync_dashboard_data_views'; export { syncDashboardContainerInput } from './sync_dashboard_container_input'; export { loadDashboardHistoryLocationState } from './load_dashboard_history_location_state'; export { buildDashboardContainer, tryDestroyDashboardContainer } from './build_dashboard_container'; diff --git a/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts index 03a03842c0e66..45eda98dcc498 100644 --- a/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts @@ -28,7 +28,7 @@ export const loadSavedDashboardState = async ({ query, history, notifications, - indexPatterns, + dataViews, savedDashboards, usageCollection, savedDashboardId, @@ -51,7 +51,7 @@ export const loadSavedDashboardState = async ({ notifications.toasts.addWarning(getDashboard60Warning()); return; } - await indexPatterns.ensureDefaultDataView(); + await dataViews.ensureDefaultDataView(); try { const savedDashboard = (await savedDashboards.get({ id: savedDashboardId, diff --git a/src/plugins/dashboard/public/application/lib/save_dashboard.ts b/src/plugins/dashboard/public/application/lib/save_dashboard.ts index 5a699eb116401..0be2211d4c2fc 100644 --- a/src/plugins/dashboard/public/application/lib/save_dashboard.ts +++ b/src/plugins/dashboard/public/application/lib/save_dashboard.ts @@ -8,6 +8,7 @@ import _ from 'lodash'; +import { isFilterPinned } from '@kbn/es-query'; import { convertTimeToUTCString } from '.'; import { NotificationsStart } from '../../services/core'; import { DashboardSavedObject } from '../../saved_dashboards'; @@ -16,7 +17,7 @@ import { SavedObjectSaveOpts } from '../../services/saved_objects'; import { dashboardSaveToastStrings } from '../../dashboard_strings'; import { getHasTaggingCapabilitiesGuard } from './dashboard_tagging'; import { SavedObjectsTaggingApi } from '../../services/saved_objects_tagging_oss'; -import { RefreshInterval, TimefilterContract, esFilters } from '../../services/data'; +import { RefreshInterval, TimefilterContract } from '../../services/data'; import { convertPanelStateToSavedDashboardPanel } from '../../../common/embeddable/embeddable_saved_object_converters'; import { DashboardSessionStorage } from './dashboard_session_storage'; import { serializeControlGroupToDashboardSavedObject } from './dashboard_control_group'; @@ -81,9 +82,7 @@ export const saveDashboard = async ({ savedDashboard.refreshInterval = savedDashboard.timeRestore ? timeRestoreObj : undefined; // only save unpinned filters - const unpinnedFilters = savedDashboard - .getFilters() - .filter((filter) => !esFilters.isFilterPinned(filter)); + const unpinnedFilters = savedDashboard.getFilters().filter((filter) => !isFilterPinned(filter)); savedDashboard.searchSource.setField('filter', unpinnedFilters); try { diff --git a/src/plugins/dashboard/public/application/lib/sync_dashboard_container_input.ts b/src/plugins/dashboard/public/application/lib/sync_dashboard_container_input.ts index 0fa7487390cd8..d3930cb5c0621 100644 --- a/src/plugins/dashboard/public/application/lib/sync_dashboard_container_input.ts +++ b/src/plugins/dashboard/public/application/lib/sync_dashboard_container_input.ts @@ -10,8 +10,9 @@ import _ from 'lodash'; import { Subscription } from 'rxjs'; import { debounceTime, tap } from 'rxjs/operators'; +import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query'; import { DashboardContainer } from '../embeddable'; -import { esFilters, Filter, Query } from '../../services/data'; +import { Query } from '../../services/data'; import { DashboardConstants, DashboardSavedObject } from '../..'; import { setControlGroupState, @@ -96,13 +97,7 @@ export const applyContainerChangesToState = ({ return; } const { filterManager } = query; - if ( - !esFilters.compareFilters( - input.filters, - filterManager.getFilters(), - esFilters.COMPARE_ALL_OPTIONS - ) - ) { + if (!compareFilters(input.filters, filterManager.getFilters(), COMPARE_ALL_OPTIONS)) { // Add filters modifies the object passed to it, hence the clone deep. filterManager.addFilters(_.cloneDeep(input.filters)); applyFilters(latestState.query, input.filters); diff --git a/src/plugins/dashboard/public/application/lib/sync_dashboard_index_patterns.ts b/src/plugins/dashboard/public/application/lib/sync_dashboard_data_views.ts similarity index 56% rename from src/plugins/dashboard/public/application/lib/sync_dashboard_index_patterns.ts rename to src/plugins/dashboard/public/application/lib/sync_dashboard_data_views.ts index 5460ef7b00037..63cecaa76fb2f 100644 --- a/src/plugins/dashboard/public/application/lib/sync_dashboard_index_patterns.ts +++ b/src/plugins/dashboard/public/application/lib/sync_dashboard_data_views.ts @@ -13,48 +13,51 @@ import { distinctUntilChanged, switchMap, filter, mapTo, map } from 'rxjs/operat import { DashboardContainer } from '..'; import { isErrorEmbeddable } from '../../services/embeddable'; -import { IndexPattern, IndexPatternsContract } from '../../services/data'; +import { DataViewsContract } from '../../services/data'; +import { DataView } from '../../services/data_views'; -interface SyncDashboardIndexPatternsProps { +interface SyncDashboardDataViewsProps { dashboardContainer: DashboardContainer; - indexPatterns: IndexPatternsContract; - onUpdateIndexPatterns: (newIndexPatterns: IndexPattern[]) => void; + dataViews: DataViewsContract; + onUpdateDataViews: (newDataViews: DataView[]) => void; } -export const syncDashboardIndexPatterns = ({ +export const syncDashboardDataViews = ({ dashboardContainer, - indexPatterns, - onUpdateIndexPatterns, -}: SyncDashboardIndexPatternsProps) => { - const updateIndexPatternsOperator = pipe( + dataViews, + onUpdateDataViews, +}: SyncDashboardDataViewsProps) => { + const updateDataViewsOperator = pipe( filter((container: DashboardContainer) => !!container && !isErrorEmbeddable(container)), - map((container: DashboardContainer): IndexPattern[] | undefined => { - let panelIndexPatterns: IndexPattern[] = []; + map((container: DashboardContainer): DataView[] | undefined => { + let panelDataViews: DataView[] = []; Object.values(container.getChildIds()).forEach((id) => { const embeddableInstance = container.getChild(id); if (isErrorEmbeddable(embeddableInstance)) return; - const embeddableIndexPatterns = (embeddableInstance.getOutput() as any).indexPatterns; - if (!embeddableIndexPatterns) return; - panelIndexPatterns.push(...embeddableIndexPatterns); + const embeddableDataViews = ( + embeddableInstance.getOutput() as { indexPatterns: DataView[] } + ).indexPatterns; + if (!embeddableDataViews) return; + panelDataViews.push(...embeddableDataViews); }); if (container.controlGroup) { - panelIndexPatterns.push(...(container.controlGroup.getOutput().dataViews ?? [])); + panelDataViews.push(...(container.controlGroup.getOutput().dataViews ?? [])); } - panelIndexPatterns = uniqBy(panelIndexPatterns, 'id'); + panelDataViews = uniqBy(panelDataViews, 'id'); /** * If no index patterns have been returned yet, and there is at least one embeddable which * hasn't yet loaded, defer the loading of the default index pattern by returning undefined. */ if ( - panelIndexPatterns.length === 0 && + panelDataViews.length === 0 && Object.keys(container.getOutput().embeddableLoaded).length > 0 && Object.values(container.getOutput().embeddableLoaded).some((value) => value === false) ) { return; } - return panelIndexPatterns; + return panelDataViews; }), distinctUntilChanged((a, b) => deepEqual( @@ -63,17 +66,17 @@ export const syncDashboardIndexPatterns = ({ ) ), // using switchMap for previous task cancellation - switchMap((panelIndexPatterns?: IndexPattern[]) => { + switchMap((panelDataViews?: DataView[]) => { return new Observable((observer) => { - if (!panelIndexPatterns) return; - if (panelIndexPatterns.length > 0) { + if (!panelDataViews) return; + if (panelDataViews.length > 0) { if (observer.closed) return; - onUpdateIndexPatterns(panelIndexPatterns); + onUpdateDataViews(panelDataViews); observer.complete(); } else { - indexPatterns.getDefault().then((defaultIndexPattern) => { + dataViews.getDefault().then((defaultDataView) => { if (observer.closed) return; - onUpdateIndexPatterns([defaultIndexPattern as IndexPattern]); + onUpdateDataViews([defaultDataView as DataView]); observer.complete(); }); } @@ -81,11 +84,11 @@ export const syncDashboardIndexPatterns = ({ }) ); - const indexPatternSources = [dashboardContainer.getOutput$()]; + const dataViewSources = [dashboardContainer.getOutput$()]; if (dashboardContainer.controlGroup) - indexPatternSources.push(dashboardContainer.controlGroup.getOutput$()); + dataViewSources.push(dashboardContainer.controlGroup.getOutput$()); - return combineLatest(indexPatternSources) - .pipe(mapTo(dashboardContainer), updateIndexPatternsOperator) + return combineLatest(dataViewSources) + .pipe(mapTo(dashboardContainer), updateDataViewsOperator) .subscribe(); }; diff --git a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts index 36b8b57cfdbd8..a6f80c157bee8 100644 --- a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts +++ b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts @@ -8,10 +8,10 @@ import { getDashboardListItemLink } from './get_dashboard_list_item_link'; import { ApplicationStart } from 'kibana/public'; -import { esFilters } from '../../../../data/public'; import { createHashHistory } from 'history'; import { createKbnUrlStateStorage } from '../../../../kibana_utils/public'; import { GLOBAL_STATE_STORAGE_KEY } from '../../url_generator'; +import { FilterStateStore } from '@kbn/es-query'; const DASHBOARD_ID = '13823000-99b9-11ea-9eb6-d9e8adceb647'; @@ -118,7 +118,7 @@ describe('when global filters change', () => { }, query: { query: 'q1' }, $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, + store: FilterStateStore.GLOBAL_STATE, }, }, ]; diff --git a/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts b/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts index 616fe56102df9..656f5672e38c0 100644 --- a/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts +++ b/src/plugins/dashboard/public/application/test_helpers/make_default_services.ts @@ -13,7 +13,7 @@ import { UrlForwardingStart } from '../../../../url_forwarding/public'; import { NavigationPublicPluginStart } from '../../services/navigation'; import { DashboardAppServices, DashboardAppCapabilities } from '../../types'; import { embeddablePluginMock } from '../../../../embeddable/public/mocks'; -import { IndexPatternsContract, SavedQueryService } from '../../services/data'; +import { DataViewsContract, SavedQueryService } from '../../services/data'; import { savedObjectsPluginMock } from '../../../../saved_objects/public/mocks'; import { screenshotModePluginMock } from '../../../../screenshot_mode/public/mocks'; import { visualizationsPluginMock } from '../../../../visualizations/public/mocks'; @@ -83,7 +83,7 @@ export function makeDefaultServices(): DashboardAppServices { savedObjectsClient: core.savedObjects.client, dashboardCapabilities: defaultCapabilities, data: dataPluginMock.createStartContract(), - indexPatterns: {} as IndexPatternsContract, + dataViews: {} as DataViewsContract, savedQueryService: {} as SavedQueryService, scopedHistory: () => ({} as ScopedHistory), setHeaderActionMenu: (mountPoint) => {}, diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 005d40a90f38f..eb251ad41f62b 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -525,7 +525,7 @@ export function DashboardTopNav({ showDatePicker, showFilterBar, setMenuMountPoint: embedSettings ? undefined : setHeaderActionMenu, - indexPatterns: dashboardAppState.indexPatterns, + indexPatterns: dashboardAppState.dataViews, showSaveQuery: dashboardCapabilities.saveQuery, useDefaultBehaviors: true, savedQuery: state.savedQuery, diff --git a/src/plugins/dashboard/public/locator.test.ts b/src/plugins/dashboard/public/locator.test.ts index f3f5aec9f478c..11ec16908b811 100644 --- a/src/plugins/dashboard/public/locator.test.ts +++ b/src/plugins/dashboard/public/locator.test.ts @@ -9,7 +9,7 @@ import { DashboardAppLocatorDefinition } from './locator'; import { hashedItemStore } from '../../kibana_utils/public'; import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; -import { esFilters } from '../../data/public'; +import { FilterStateStore } from '@kbn/es-query'; describe('dashboard locator', () => { beforeEach(() => { @@ -79,7 +79,7 @@ describe('dashboard locator', () => { }, query: { query: 'hi' }, $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, + store: FilterStateStore.GLOBAL_STATE, }, }, ], diff --git a/src/plugins/dashboard/public/locator.ts b/src/plugins/dashboard/public/locator.ts index b6655e246de36..42efb521cf6e5 100644 --- a/src/plugins/dashboard/public/locator.ts +++ b/src/plugins/dashboard/public/locator.ts @@ -8,11 +8,11 @@ import type { SerializableRecord } from '@kbn/utility-types'; import { flow } from 'lodash'; -import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public'; +import { type Filter } from '@kbn/es-query'; +import type { TimeRange, Query, QueryState, RefreshInterval } from '../../data/public'; import type { LocatorDefinition, LocatorPublic } from '../../share/public'; import type { SavedDashboardPanel } from '../common/types'; import type { RawDashboardState } from './types'; -import { esFilters } from '../../data/public'; import { setStateToKbnUrl } from '../../kibana_utils/public'; import { ViewMode } from '../../embeddable/public'; import { DashboardConstants } from './dashboard_constants'; @@ -152,12 +152,14 @@ export class DashboardAppLocatorDefinition implements LocatorDefinition( '_g', cleanEmptyKeys({ time: params.timeRange, - filters: filters?.filter((f) => esFilters.isFilterPinned(f)), + filters: filters?.filter((f) => isFilterPinned(f)), refreshInterval: params.refreshInterval, }), { useHash }, diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 7f784d43c0cb7..6554520fca101 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -33,7 +33,7 @@ import { UiActionsSetup, UiActionsStart } from './services/ui_actions'; import { PresentationUtilPluginStart } from './services/presentation_util'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from './services/home'; import { NavigationPublicPluginStart as NavigationStart } from './services/navigation'; -import { DataPublicPluginSetup, DataPublicPluginStart, esFilters } from './services/data'; +import { DataPublicPluginSetup, DataPublicPluginStart } from './services/data'; import { SharePluginSetup, SharePluginStart, UrlGeneratorContract } from './services/share'; import type { SavedObjectTaggingOssPluginStart } from './services/saved_objects_tagging_oss'; import type { @@ -253,10 +253,13 @@ export class DashboardPlugin filter( ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) ), - map(({ state }) => ({ - ...state, - filters: state.filters?.filter(esFilters.isFilterPinned), - })) + map(async ({ state }) => { + const { isFilterPinned } = await import('@kbn/es-query'); + return { + ...state, + filters: state.filters?.filter(isFilterPinned), + }; + }) ), }, ], diff --git a/src/plugins/dashboard/public/services/data_views.ts b/src/plugins/dashboard/public/services/data_views.ts new file mode 100644 index 0000000000000..4fb2bbaf08503 --- /dev/null +++ b/src/plugins/dashboard/public/services/data_views.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from '../../../data_views/public'; diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index b7b146aeba348..4de07974203a7 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -17,22 +17,25 @@ import type { KibanaExecutionContext, } from 'kibana/public'; import { History } from 'history'; +import type { Filter } from '@kbn/es-query'; import { AnyAction, Dispatch } from 'redux'; import { BehaviorSubject, Subject } from 'rxjs'; -import { Query, Filter, IndexPattern, RefreshInterval, TimeRange } from './services/data'; -import { ContainerInput, EmbeddableInput, ViewMode } from './services/embeddable'; + +import { DataView } from './services/data_views'; import { SharePluginStart } from './services/share'; import { EmbeddableStart } from './services/embeddable'; import { DashboardSessionStorage } from './application/lib'; import { UrlForwardingStart } from '../../url_forwarding/public'; import { UsageCollectionSetup } from './services/usage_collection'; import { NavigationPublicPluginStart } from './services/navigation'; +import { Query, RefreshInterval, TimeRange } from './services/data'; import { DashboardPanelState, SavedDashboardPanel } from '../common/types'; import { SavedObjectsTaggingApi } from './services/saved_objects_tagging_oss'; -import { DataPublicPluginStart, IndexPatternsContract } from './services/data'; +import { DataPublicPluginStart, DataViewsContract } from './services/data'; +import { ContainerInput, EmbeddableInput, ViewMode } from './services/embeddable'; import { SavedObjectLoader, SavedObjectsStart } from './services/saved_objects'; -import { IKbnUrlStateStorage } from './services/kibana_utils'; import type { ScreenshotModePluginStart } from './services/screenshot_mode'; +import { IKbnUrlStateStorage } from './services/kibana_utils'; import type { DashboardContainer, DashboardSavedObject } from '.'; import { VisualizationsStart } from '../../visualizations/public'; import { DashboardAppLocatorParams } from './locator'; @@ -102,7 +105,7 @@ export interface DashboardContainerInput extends ContainerInput { */ export interface DashboardAppState { hasUnsavedChanges?: boolean; - indexPatterns?: IndexPattern[]; + dataViews?: DataView[]; updateLastSavedState?: () => void; resetToLastSavedState?: () => void; savedDashboard?: DashboardSavedObject; @@ -119,7 +122,7 @@ export interface DashboardAppState { export type DashboardBuildContext = Pick< DashboardAppServices, | 'embeddable' - | 'indexPatterns' + | 'dataViews' | 'savedDashboards' | 'usageCollection' | 'initializerContext' @@ -198,7 +201,7 @@ export interface DashboardAppServices { savedDashboards: SavedObjectLoader; scopedHistory: () => ScopedHistory; visualizations: VisualizationsStart; - indexPatterns: IndexPatternsContract; + dataViews: DataViewsContract; usageCollection?: UsageCollectionSetup; navigation: NavigationPublicPluginStart; dashboardCapabilities: DashboardAppCapabilities; diff --git a/src/plugins/dashboard/public/url_generator.test.ts b/src/plugins/dashboard/public/url_generator.test.ts index 9a1204f116c7f..f1035d7cc1389 100644 --- a/src/plugins/dashboard/public/url_generator.test.ts +++ b/src/plugins/dashboard/public/url_generator.test.ts @@ -9,8 +9,8 @@ import { createDashboardUrlGenerator } from './url_generator'; import { hashedItemStore } from '../../kibana_utils/public'; import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; -import { esFilters, Filter } from '../../data/public'; import { SavedObjectLoader } from '../../saved_objects/public'; +import { type Filter, FilterStateStore } from '@kbn/es-query'; const APP_BASE_PATH: string = 'xyz/app/dashboards'; @@ -99,7 +99,7 @@ describe('dashboard url generator', () => { }, query: { query: 'hi' }, $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, + store: FilterStateStore.GLOBAL_STATE, }, }, ], diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts index e0cd410ce5e8f..ed8f87ad9b51b 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -25,7 +25,7 @@ import { convertSavedDashboardPanelToPanelState, } from '../../common/embeddable/embeddable_saved_object_converters'; import { SavedObjectEmbeddableInput } from '../../../embeddable/common'; -import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../data/common'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../../data/common'; import { mergeMigrationFunctionMaps, MigrateFunction, @@ -49,7 +49,7 @@ function migrateIndexPattern(doc: DashboardDoc700To720) { searchSource.indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; doc.references.push({ name: searchSource.indexRefName, - type: INDEX_PATTERN_SAVED_OBJECT_TYPE, + type: DATA_VIEW_SAVED_OBJECT_TYPE, id: searchSource.index, }); delete searchSource.index; @@ -62,7 +62,7 @@ function migrateIndexPattern(doc: DashboardDoc700To720) { filterRow.meta.indexRefName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; doc.references.push({ name: filterRow.meta.indexRefName, - type: INDEX_PATTERN_SAVED_OBJECT_TYPE, + type: DATA_VIEW_SAVED_OBJECT_TYPE, id: filterRow.meta.index, }); delete filterRow.meta.index; diff --git a/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts b/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts index 8980bd1903323..4000bed0c28ac 100644 --- a/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts +++ b/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts @@ -6,13 +6,13 @@ * Side Public License, v 1. */ -import { esFilters, Filter } from 'src/plugins/data/public'; +import { FilterStateStore, Filter } from '@kbn/es-query'; import { moveFiltersToQuery, Pre600FilterQuery } from './move_filters_to_query'; const filter: Filter = { meta: { disabled: false, negate: false, alias: '' }, query: {}, - $state: { store: esFilters.FilterStateStore.APP_STATE }, + $state: { store: FilterStateStore.APP_STATE }, }; const queryFilter: Pre600FilterQuery = { @@ -27,7 +27,7 @@ test('Migrates an old filter query into the query field', () => { expect(newSearchSource).toEqual({ filter: [ { - $state: { store: esFilters.FilterStateStore.APP_STATE }, + $state: { store: FilterStateStore.APP_STATE }, meta: { alias: '', disabled: false, diff --git a/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts b/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts index ddd1c45841b9c..e2ea076de7743 100644 --- a/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts +++ b/src/plugins/dashboard/server/saved_objects/replace_index_pattern_reference.ts @@ -7,14 +7,14 @@ */ import type { SavedObjectMigrationFn } from 'kibana/server'; -import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../../data/common'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../../data/common'; export const replaceIndexPatternReference: SavedObjectMigrationFn = (doc) => ({ ...doc, references: Array.isArray(doc.references) ? doc.references.map((reference) => { if (reference.type === 'index_pattern') { - reference.type = INDEX_PATTERN_SAVED_OBJECT_TYPE; + reference.type = DATA_VIEW_SAVED_OBJECT_TYPE; } return reference; }) diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 680d06780543a..55049447aee57 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -11,6 +11,7 @@ { "path": "../../core/tsconfig.json" }, { "path": "../inspector/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, + { "path": "../data_views/tsconfig.json" }, { "path": "../kibana_utils/tsconfig.json" }, { "path": "../share/tsconfig.json" }, { "path": "../controls/tsconfig.json" }, From ca10263ed64462c8bd739560162358ef44856535 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Mon, 7 Feb 2022 14:26:51 -0800 Subject: [PATCH 06/44] [DOCS] Fixes links in Update docs (#124892) * [DOCS] Fixes links in Update docs * [DOCS] Fixes another formatting error --- docs/redirects.asciidoc | 6 +- .../setup/upgrade/upgrade-migrations.asciidoc | 60 ++++++++++++------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index ff6ccbd6fab36..163fed04578ef 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -386,12 +386,12 @@ This content has moved. Refer to <>. This content has moved. Refer to <>. -[role="exclude" logging-configuration-changes] -== Logging configuration changes +[role="exclude",id="logging-configuration-changes"] +== Logging configuration changes This content has moved. Refer to <>. -[role="exclude" upgrade-migrations] +[role="exclude",id="upgrade-migrations"] == Upgrade migrations This content has moved. Refer to <>. diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc index fc921f9118bdf..7136011a4f8f8 100644 --- a/docs/setup/upgrade/upgrade-migrations.asciidoc +++ b/docs/setup/upgrade/upgrade-migrations.asciidoc @@ -15,11 +15,11 @@ WARNING: The following instructions assumes {kib} is using the default index nam [[upgrade-migrations-process]] ==== Background -Saved objects are stored in two indices: +Saved objects are stored in two indices: * `.kibana_{kibana_version}_001`, e.g. for Kibana v7.12.0 `.kibana_7.12.0_001`. * `.kibana_task_manager_{kibana_version}_001`, e.g. for Kibana v7.12.0 `.kibana_task_manager_7.12.0_001`. - + The index aliases `.kibana` and `.kibana_task_manager` will always point to the most up-to-date saved object indices. @@ -29,18 +29,18 @@ The first time a newer {kib} starts, it will first perform an upgrade migration [options="header"] |======================= |Upgrading from version | Outdated index (alias) -| 6.0.0 through 6.4.x | `.kibana` +| 6.0.0 through 6.4.x | `.kibana` `.kibana_task_manager_7.12.0_001` (`.kibana_task_manager` alias) | 6.5.0 through 7.3.x | `.kibana_N` (`.kibana` alias) -| 7.4.0 through 7.11.x -| `.kibana_N` (`.kibana` alias) +| 7.4.0 through 7.11.x +| `.kibana_N` (`.kibana` alias) `.kibana_task_manager_N` (`.kibana_task_manager` alias) |======================= ==== Upgrading multiple {kib} instances -When upgrading several {kib} instances connected to the same {es} cluster, ensure that all outdated instances are shutdown before starting the upgrade. +When upgrading several {kib} instances connected to the same {es} cluster, ensure that all outdated instances are shutdown before starting the upgrade. Kibana does not support rolling upgrades. However, once outdated instances are shutdown, all upgraded instances can be started in parallel in which case all instances will participate in the upgrade migration in parallel. @@ -64,13 +64,15 @@ Error: Unable to complete saved object migrations for the [.kibana] index. Pleas Error: Unable to complete saved object migrations for the [.kibana] index. Please check the health of your Elasticsearch cluster and try again. Error: [timeout_exception]: Timed out waiting for completion of [org.elasticsearch.index.reindex.BulkByScrollTask@6a74c54] -------------------------------------------- -See https://github.com/elastic/kibana/issues/95321 for instructions to work around this issue. - +Instructions to work around this issue are in https://github.com/elastic/kibana/issues/95321[this GitHub issue]. + [float] ===== Corrupt saved objects We highly recommend testing your {kib} upgrade in a development cluster to discover and remedy problems caused by corrupt documents, especially when there are custom integrations creating saved objects in your environment. -Saved objects that were corrupted through manual editing or integrations will cause migration failures with a log message like `Failed to transform document. Transform: index-pattern:7.0.0\n Doc: {...}` or `Unable to migrate the corrupt Saved Object document ...`. Corrupt documents will have to be fixed or deleted before an upgrade migration can succeed. +Saved objects that were corrupted through manual editing or integrations will cause migration +failures with a log message like `Unable to migrate the corrupt Saved Object document ...`. +Corrupt documents will have to be fixed or deleted before an upgrade migration can succeed. For example, given the following error message: @@ -101,7 +103,7 @@ DELETE .kibana_7.12.0_001/_doc/marketing_space:dashboard:e3c5fc71-ac71-4805-bcab -------------------------------------------- . Restart {kib}. - ++ In this example, the Dashboard with ID `e3c5fc71-ac71-4805-bcab-2bcc9cc93275` that belongs to the space `marketing_space` **will no longer be available**. Be sure you have a snapshot before you delete the corrupt document. If restoring from a snapshot is not an option, it is recommended to also delete the `temp` and `target` indices the migration created before restarting {kib} and retrying. @@ -112,15 +114,16 @@ Matching index templates which specify `settings.refresh_interval` or `mappings` Prevention: narrow down the index patterns of any user-defined index templates to ensure that these won't apply to new `.kibana*` indices. -Note: {kib} < 6.5 creates it's own index template called `kibana_index_template:.kibana` and index pattern `.kibana`. This index template will not interfere and does not need to be changed or removed. +NOTE: {kib} < 6.5 creates it's own index template called `kibana_index_template:.kibana` +and uses an index pattern of `.kibana`. This index template will not interfere and does not need to be changed or removed. [float] ===== An unhealthy {es} cluster Problems with your {es} cluster can prevent {kib} upgrades from succeeding. Ensure that your cluster has: - * enough free disk space, at least twice the amount of storage taken up by the `.kibana` and `.kibana_task_manager` indices - * sufficient heap size - * a "green" cluster status + * Enough free disk space, at least twice the amount of storage taken up by the `.kibana` and `.kibana_task_manager` indices + * Sufficient heap size + * A "green" cluster status [float] ===== Different versions of {kib} connected to the same {es} index @@ -134,20 +137,32 @@ For {kib} versions prior to 7.5.1, if the task manager index is set to `.tasks` [[resolve-migrations-failures]] ==== Resolving migration failures -If {kib} terminates unexpectedly while migrating a saved object index it will automatically attempt to perform the migration again once the process has restarted. Do not delete any saved objects indices to attempt to fix a failed migration. Unlike previous versions, {kib} version 7.12.0 and later does not require deleting any indices to release a failed migration lock. +If {kib} terminates unexpectedly while migrating a saved object index it will automatically attempt to +perform the migration again once the process has restarted. Do not delete any saved objects indices to +attempt to fix a failed migration. Unlike previous versions, {kib} version 7.12.0 and +later does not require deleting any indices to release a failed migration lock. -If upgrade migrations fail repeatedly, follow the advice in (preventing migration failures)[preventing-migration-failures]. Once the root cause for the migration failure has been addressed, {kib} will automatically retry the migration without any further intervention. If you're unable to resolve a failed migration following these steps, please contact support. +If upgrade migrations fail repeatedly, follow the advice in +<>. +Once the root cause for the migration failure has been addressed, +{kib} will automatically retry the migration without any further intervention. +If you're unable to resolve a failed migration following these steps, please contact support. [float] [[upgrade-migrations-rolling-back]] ==== Rolling back to a previous version of {kib} -If you've followed the advice in (preventing migration failures)[preventing-migration-failures] and (resolving migration failures)[resolve-migrations-failures] and {kib} is still not able to upgrade successfully, you might choose to rollback {kib} until you're able to identify and fix the root cause. +If you've followed the advice in <> +and <> and +{kib} is still not able to upgrade successfully, you might choose to rollback {kib} until +you're able to identify and fix the root cause. -WARNING: Before rolling back {kib}, ensure that the version you wish to rollback to is compatible with your {es} cluster. If the version you're rolling back to is not compatible, you will have to also rollback {es}. + +WARNING: Before rolling back {kib}, ensure that the version you wish to rollback to is compatible with +your {es} cluster. If the version you're rolling back to is not compatible, you will have to also rollback {es}. Any changes made after an upgrade will be lost when rolling back to a previous version. -In order to rollback after a failed upgrade migration, the saved object indices have to be rolled back to be compatible with the previous {kibana} version. +In order to rollback after a failed upgrade migration, the saved object indices have to be +rolled back to be compatible with the previous {kib} version. [float] ===== Rollback by restoring a backup snapshot: @@ -164,8 +179,11 @@ In order to rollback after a failed upgrade migration, the saved object indices 1. Shutdown all {kib} instances to be 100% sure that there are no {kib} instances currently performing a migration. 2. {ref}/snapshots-take-snapshot.html[Take a snapshot] that includes the `kibana` feature state. Snapshots include this feature state by default. -3. Delete the version specific indices created by the failed upgrade migration. E.g. if you wish to rollback from a failed upgrade to v7.12.0 `DELETE /.kibana_7.12.0_*,.kibana_task_manager_7.12.0_*` -4. Inspect the output of `GET /_cat/aliases`. If either the `.kibana` and/or `.kibana_task_manager` alias is missing, these will have to be created manually. Find the latest index from the output of `GET /_cat/indices` and create the missing alias to point to the latest index. E.g. if the `.kibana` alias was missing and the latest index is `.kibana_3` create a new alias with `POST /.kibana_3/_aliases/.kibana`. +3. Delete the version specific indices created by the failed upgrade migration. For example, if you wish to rollback from a failed upgrade to v7.12.0 `DELETE /.kibana_7.12.0_*,.kibana_task_manager_7.12.0_*` +4. Inspect the output of `GET /_cat/aliases`. +If either the `.kibana` and/or `.kibana_task_manager` alias is missing, these will have to be created manually. +Find the latest index from the output of `GET /_cat/indices` and create the missing alias to point to the latest index. +For example. if the `.kibana` alias was missing and the latest index is `.kibana_3` create a new alias with `POST /.kibana_3/_aliases/.kibana`. 5. Remove the write block from the rollback indices. `PUT /.kibana,.kibana_task_manager/_settings {"index.blocks.write": false}` 6. Start up {kib} on the older version you wish to rollback to. From 912f4b910f978bd23860a3ddc6c41faf32644152 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 7 Feb 2022 17:38:10 -0500 Subject: [PATCH 07/44] [Fleet] do not allow empty name for agent policy (#124853) --- .../agent_policy/create_package_policy_page/index.tsx | 7 ++++++- .../plugins/fleet/server/types/models/agent_policy.ts | 8 +++++++- .../apis/agent_policy/agent_policy.ts | 11 +++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index 743ff40ecf5e6..98e96ce598561 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -206,7 +206,12 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { updatedAgentPolicy: NewAgentPolicy ) => { if (selectedTab === SelectedPolicyTab.NEW) { - if (!updatedAgentPolicy.name || !updatedAgentPolicy.namespace) { + if ( + !updatedAgentPolicy.name || + updatedAgentPolicy.name.trim() === '' || + !updatedAgentPolicy.namespace || + updatedAgentPolicy.namespace.trim() === '' + ) { setHasAgentPolicyError(true); } else { setHasAgentPolicyError(false); diff --git a/x-pack/plugins/fleet/server/types/models/agent_policy.ts b/x-pack/plugins/fleet/server/types/models/agent_policy.ts index e1398aea63634..d15d73fca7332 100644 --- a/x-pack/plugins/fleet/server/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/agent_policy.ts @@ -11,9 +11,15 @@ import { agentPolicyStatuses, dataTypes } from '../../../common'; import { PackagePolicySchema, NamespaceSchema } from './package_policy'; +function validateNonEmptyString(val: string) { + if (val.trim() === '') { + return 'Invalid empty string'; + } +} + export const AgentPolicyBaseSchema = { id: schema.maybe(schema.string()), - name: schema.string({ minLength: 1 }), + name: schema.string({ minLength: 1, validate: validateNonEmptyString }), namespace: NamespaceSchema, description: schema.maybe(schema.string()), is_managed: schema.maybe(schema.boolean()), diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index e00ea43a02406..417e0c76a9e6b 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -101,6 +101,17 @@ export default function (providerContext: FtrProviderContext) { .expect(400); }); + it('should return a 400 with an empty name', async () => { + await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: ' ', + namespace: 'default', + }) + .expect(400); + }); + it('should return a 400 with an invalid namespace', async () => { await supertest .post(`/api/fleet/agent_policies`) From 1f0f399d6534cf04ff94fc14d207a97589001d3d Mon Sep 17 00:00:00 2001 From: smnschneider <95302847+smnschneider@users.noreply.github.com> Date: Mon, 7 Feb 2022 23:38:58 +0100 Subject: [PATCH 08/44] [Fleet] Change error message for allowAgentUpgradeSourceUri (#124706) --- x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts | 2 +- x-pack/test/fleet_api_integration/apis/agents/upgrade.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts index bf386e7f463a7..472f7378028bc 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts @@ -133,7 +133,7 @@ export const checkVersionIsSame = (version: string, kibanaVersion: string) => { const checkSourceUriAllowed = (sourceUri?: string) => { if (sourceUri && !appContextService.getConfig()?.developer?.allowAgentUpgradeSourceUri) { throw new Error( - `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to enable.` + `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to true.` ); } }; diff --git a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts index 8901c3166ca14..57e57a6524b0e 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/upgrade.ts @@ -174,7 +174,7 @@ export default function (providerContext: FtrProviderContext) { .expect(400); expect(res.body.message).to.eql( - `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to enable.` + `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to true.` ); }); it('should respond 400 if trying to upgrade an agent that is unenrolling', async () => { @@ -591,7 +591,7 @@ export default function (providerContext: FtrProviderContext) { }) .expect(400); expect(res.body.message).to.eql( - `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to enable.` + `source_uri is not allowed or recommended in production. Set xpack.fleet.developer.allowAgentUpgradeSourceUri in kibana.yml to true.` ); }); From eb5ab3e23e667df438449f2974095d1d7024ab8f Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Mon, 7 Feb 2022 16:51:00 -0600 Subject: [PATCH 09/44] [cloud first testing] Purge vault when deployment is removed (#124761) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .buildkite/scripts/steps/cloud/purge.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.buildkite/scripts/steps/cloud/purge.js b/.buildkite/scripts/steps/cloud/purge.js index 0eccb55cef830..b14a3be8f8daf 100644 --- a/.buildkite/scripts/steps/cloud/purge.js +++ b/.buildkite/scripts/steps/cloud/purge.js @@ -50,6 +50,9 @@ for (const deployment of deploymentsToPurge) { console.log(`Scheduling deployment for deletion: ${deployment.name} / ${deployment.id}`); try { execSync(`ecctl deployment shutdown --force '${deployment.id}'`, { stdio: 'inherit' }); + execSync(`vault delete secret/kibana-issues/dev/cloud-deploy/${deployment.name}`, { + stdio: 'inherit', + }); } catch (ex) { console.error(ex.toString()); } From 5538967ba4b555842bc72cc4d601e6ef772a8265 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 7 Feb 2022 18:03:17 -0500 Subject: [PATCH 10/44] [Uptime] add synthetics service sync errors (#124051) * uptime - add synthetics sync errors * update imports * update content * adjust content and flow * update toasts * Update x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar_errors.test.tsx * adjust tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../monitor_management/locations.ts | 21 ++++++ x-pack/plugins/uptime/e2e/journeys/index.ts | 2 +- .../e2e/journeys/monitor_name.journey.ts | 7 +- .../e2e/page_objects/monitor_management.tsx | 2 +- .../action_bar/action_bar.tsx | 71 ++++++++++++++---- .../action_bar/action_bar_errors.test.tsx | 75 +++++++++++++++++++ .../monitor_management/mocks/index.ts | 8 ++ .../monitor_management/mocks/locations.ts | 35 +++++++++ .../public/state/api/monitor_management.ts | 3 +- .../synthetics_service/service_api_client.ts | 10 ++- 10 files changed, 212 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar_errors.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/mocks/index.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor_management/mocks/locations.ts diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts b/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts index 6cef41347bbf6..26e3d726a10c0 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor_management/locations.ts @@ -29,6 +29,26 @@ export const ServiceLocationCodec = t.interface({ url: t.string, }); +export const ServiceLocationErrors = t.array( + t.intersection([ + t.interface({ + locationId: t.string, + error: t.interface({ + reason: t.string, + status: t.number, + }), + }), + t.partial({ + failed_monitors: t.array( + t.interface({ + id: t.string, + message: t.string, + }) + ), + }), + ]) +); + export const ServiceLocationsCodec = t.array(ServiceLocationCodec); export const isServiceLocationInvalid = (location: ServiceLocation) => @@ -42,3 +62,4 @@ export type ManifestLocation = t.TypeOf; export type ServiceLocation = t.TypeOf; export type ServiceLocations = t.TypeOf; export type ServiceLocationsApiResponse = t.TypeOf; +export type ServiceLocationErrors = t.TypeOf; diff --git a/x-pack/plugins/uptime/e2e/journeys/index.ts b/x-pack/plugins/uptime/e2e/journeys/index.ts index ce197d574aa15..fe8a4960eac12 100644 --- a/x-pack/plugins/uptime/e2e/journeys/index.ts +++ b/x-pack/plugins/uptime/e2e/journeys/index.ts @@ -7,8 +7,8 @@ export * from './data_view_permissions'; export * from './uptime.journey'; -export * from './monitor_management.journey'; export * from './step_duration.journey'; export * from './alerts'; export * from './read_only_user'; export * from './monitor_name.journey'; +export * from './monitor_management.journey'; diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts index beb84a9a003a2..456d219adef05 100644 --- a/x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_name.journey.ts @@ -12,7 +12,7 @@ * 2.0. */ -import { journey, step, expect, before, Page } from '@elastic/synthetics'; +import { journey, step, expect, after, before, Page } from '@elastic/synthetics'; import { monitorManagementPageProvider } from '../page_objects/monitor_management'; import { byTestId } from './utils'; @@ -23,6 +23,11 @@ journey(`MonitorName`, async ({ page, params }: { page: Page; params: any }) => await uptime.waitForLoadingToFinish(); }); + after(async () => { + await uptime.navigateToMonitorManagement(); + await uptime.deleteMonitor(); + }); + step('Go to monitor-management', async () => { await uptime.navigateToMonitorManagement(); }); diff --git a/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx b/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx index 057ce21ec5100..fd877708f2bce 100644 --- a/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx +++ b/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx @@ -88,7 +88,7 @@ export function monitorManagementPageProvider({ } else { await page.click('text=Save monitor'); } - return await this.findByTestSubj('uptimeAddMonitorSuccess'); + return await this.findByText('Monitor added successfully.'); }, async fillCodeEditor(value: string) { diff --git a/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx index 8c9dc7ffe6275..314347331b5b3 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar.tsx @@ -17,8 +17,9 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useSelector } from 'react-redux'; import { FETCH_STATUS, useFetcher } from '../../../../../observability/public'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; import { MONITOR_MANAGEMENT_ROUTE } from '../../../../common/constants'; import { UptimeSettingsContext } from '../../../contexts'; @@ -28,6 +29,10 @@ import { SyntheticsMonitor } from '../../../../common/runtime_types'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { TestRun } from '../test_now_mode/test_now_mode'; +import { monitorManagementListSelector } from '../../../state/selectors'; + +import { kibanaService } from '../../../state/kibana_service'; + export interface ActionBarProps { monitor: SyntheticsMonitor; isValid: boolean; @@ -39,11 +44,11 @@ export interface ActionBarProps { export const ActionBar = ({ monitor, isValid, onSave, onTestNow, testRun }: ActionBarProps) => { const { monitorId } = useParams<{ monitorId: string }>(); const { basePath } = useContext(UptimeSettingsContext); + const { locations } = useSelector(monitorManagementListSelector); const [hasBeenSubmitted, setHasBeenSubmitted] = useState(false); const [isSaving, setIsSaving] = useState(false); - - const { notifications } = useKibana(); + const [isSuccessful, setIsSuccessful] = useState(false); const { data, status } = useFetcher(() => { if (!isSaving || !isValid) { @@ -55,6 +60,9 @@ export const ActionBar = ({ monitor, isValid, onSave, onTestNow, testRun }: Acti }); }, [monitor, monitorId, isValid, isSaving]); + const hasErrors = data && Object.keys(data).length; + const loading = status === FETCH_STATUS.LOADING; + const handleOnSave = useCallback(() => { if (onSave) { onSave(); @@ -75,23 +83,57 @@ export const ActionBar = ({ monitor, isValid, onSave, onTestNow, testRun }: Acti setIsSaving(false); } if (status === FETCH_STATUS.FAILURE) { - notifications.toasts.danger({ - title:

{MONITOR_FAILURE_LABEL}

, + kibanaService.toasts.addDanger({ + title: MONITOR_FAILURE_LABEL, toastLifeTimeMs: 3000, }); - } else if (status === FETCH_STATUS.SUCCESS) { - notifications.toasts.success({ - title: ( -

- {monitorId ? MONITOR_UPDATED_SUCCESS_LABEL : MONITOR_SUCCESS_LABEL} -

- ), + } else if (status === FETCH_STATUS.SUCCESS && !hasErrors && !loading) { + kibanaService.toasts.addSuccess({ + title: monitorId ? MONITOR_UPDATED_SUCCESS_LABEL : MONITOR_SUCCESS_LABEL, toastLifeTimeMs: 3000, }); + setIsSuccessful(true); + } else if (hasErrors && !loading) { + Object.values(data).forEach((location) => { + const { status: responseStatus, reason } = location.error || {}; + kibanaService.toasts.addWarning({ + title: i18n.translate('xpack.uptime.monitorManagement.service.error.title', { + defaultMessage: `Unable to sync monitor config`, + }), + text: toMountPoint( + <> +

+ {i18n.translate('xpack.uptime.monitorManagement.service.error.message', { + defaultMessage: `Your monitor was saved, but there was a problem syncing the configuration for {location}. We will automatically try again later. If this problem continues, your monitors will stop running in {location}. Please contact Support for assistance.`, + values: { + location: locations?.find((loc) => loc?.id === location.locationId)?.label, + }, + })} +

+

+ {status + ? i18n.translate('xpack.uptime.monitorManagement.service.error.status', { + defaultMessage: 'Status: {status}. ', + values: { status: responseStatus }, + }) + : null} + {reason + ? i18n.translate('xpack.uptime.monitorManagement.service.error.reason', { + defaultMessage: 'Reason: {reason}.', + values: { reason }, + }) + : null} +

+ + ), + toastLifeTimeMs: 30000, + }); + }); + setIsSuccessful(true); } - }, [data, status, notifications.toasts, isSaving, isValid, monitorId]); + }, [data, status, isSaving, isValid, monitorId, hasErrors, locations, loading]); - return status === FETCH_STATUS.SUCCESS ? ( + return isSuccessful ? ( ) : ( @@ -191,7 +233,6 @@ const MONITOR_UPDATED_SUCCESS_LABEL = i18n.translate( } ); -// TODO: Discuss error states with product const MONITOR_FAILURE_LABEL = i18n.translate( 'xpack.uptime.monitorManagement.monitorFailureMessage', { diff --git a/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar_errors.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar_errors.test.tsx new file mode 100644 index 0000000000000..f217631bfe33d --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/action_bar/action_bar_errors.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { render } from '../../../lib/helper/rtl_helpers'; +import { FETCH_STATUS } from '../../../../../observability/public'; +import { + DataStream, + HTTPFields, + ScheduleUnit, + SyntheticsMonitor, +} from '../../../../common/runtime_types'; +import { spyOnUseFetcher } from '../../../lib/helper/spy_use_fetcher'; +import * as kibana from '../../../state/kibana_service'; +import { ActionBar } from './action_bar'; +import { mockLocationsState } from '../mocks'; + +jest.mock('../../../state/kibana_service', () => ({ + ...jest.requireActual('../../../state/kibana_service'), + kibanaService: { + toasts: { + addWarning: jest.fn(), + }, + }, +})); + +const monitor: SyntheticsMonitor = { + name: 'test-monitor', + schedule: { + unit: ScheduleUnit.MINUTES, + number: '2', + }, + urls: 'https://elastic.co', + type: DataStream.HTTP, +} as unknown as HTTPFields; + +describe(' Service Errors', () => { + let useFetcher: jest.SpyInstance; + const toast = jest.fn(); + + beforeEach(() => { + useFetcher?.mockClear(); + useFetcher = spyOnUseFetcher({}); + }); + + it('Handles service errors', async () => { + jest.spyOn(kibana.kibanaService.toasts, 'addWarning').mockImplementation(toast); + useFetcher.mockReturnValue({ + data: [ + { locationId: 'us_central', error: { reason: 'Invalid config', status: 400 } }, + { locationId: 'us_central', error: { reason: 'Cannot schedule', status: 500 } }, + ], + status: FETCH_STATUS.SUCCESS, + refetch: () => {}, + }); + render(, { state: mockLocationsState }); + userEvent.click(screen.getByText('Save monitor')); + + await waitFor(() => { + expect(toast).toBeCalledTimes(2); + expect(toast).toBeCalledWith( + expect.objectContaining({ + title: 'Unable to sync monitor config', + toastLifeTimeMs: 30000, + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor_management/mocks/index.ts b/x-pack/plugins/uptime/public/components/monitor_management/mocks/index.ts new file mode 100644 index 0000000000000..1ec4437601d57 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/mocks/index.ts @@ -0,0 +1,8 @@ +/* + * 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 * from './locations'; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/mocks/locations.ts b/x-pack/plugins/uptime/public/components/monitor_management/mocks/locations.ts new file mode 100644 index 0000000000000..b4f23bed097cb --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor_management/mocks/locations.ts @@ -0,0 +1,35 @@ +/* + * 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 const mockLocation = { + label: 'US Central', + id: 'us_central', + geo: { + lat: 1, + lon: 1, + }, + url: 'url', +}; + +export const mockLocationsState = { + monitorManagementList: { + locations: [mockLocation], + list: { + monitors: [], + perPage: 10, + page: 1, + total: 0, + }, + error: { + serviceLocations: null, + monitorList: null, + }, + loading: { + serviceLocations: false, + monitorList: false, + }, + }, +}; diff --git a/x-pack/plugins/uptime/public/state/api/monitor_management.ts b/x-pack/plugins/uptime/public/state/api/monitor_management.ts index ec2806907baa1..206ba07dc4c23 100644 --- a/x-pack/plugins/uptime/public/state/api/monitor_management.ts +++ b/x-pack/plugins/uptime/public/state/api/monitor_management.ts @@ -13,6 +13,7 @@ import { ServiceLocations, SyntheticsMonitor, ServiceLocationsApiResponseCodec, + ServiceLocationErrors, } from '../../../common/runtime_types'; import { SyntheticsMonitorSavedObject } from '../../../common/types'; import { apiService } from './utils'; @@ -23,7 +24,7 @@ export const setMonitor = async ({ }: { monitor: SyntheticsMonitor; id?: string; -}): Promise => { +}): Promise => { if (id) { return await apiService.put(`${API_URLS.SYNTHETICS_MONITORS}/${id}`, monitor); } else { diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts index 596a64b4d359a..1e82ef77e083b 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts @@ -11,7 +11,11 @@ import { catchError, tap } from 'rxjs/operators'; import * as https from 'https'; import { SslConfig } from '@kbn/server-http-tools'; import { Logger } from '../../../../../../src/core/server'; -import { MonitorFields, ServiceLocations } from '../../../common/runtime_types'; +import { + MonitorFields, + ServiceLocations, + ServiceLocationErrors, +} from '../../../common/runtime_types'; import { convertToDataStreamFormat } from './formatters/convert_to_data_stream'; import { ServiceConfig } from '../../../common/config'; @@ -109,7 +113,7 @@ export class ServiceAPIClient { }); }; - const pushErrors: Array<{ locationId: string; error: Error }> = []; + const pushErrors: ServiceLocationErrors = []; const promises: Array> = []; @@ -128,7 +132,7 @@ export class ServiceAPIClient { ); }), catchError((err) => { - pushErrors.push({ locationId: id, error: err }); + pushErrors.push({ locationId: id, error: err.response?.data }); this.logger.error(err); // we don't want to throw an unhandled exception here return of(true); From be1be06522050d5ea4727441155470809ca0f008 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Mon, 7 Feb 2022 17:11:17 -0600 Subject: [PATCH 11/44] [build] Improve error message for dependencies in cloud docker image build (#124890) * [build] Improve error message for dependencies in cloud docker image build * cleanup --- .../build/tasks/download_cloud_dependencies.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/dev/build/tasks/download_cloud_dependencies.ts b/src/dev/build/tasks/download_cloud_dependencies.ts index 1207594304e64..6ecc09c21ddce 100644 --- a/src/dev/build/tasks/download_cloud_dependencies.ts +++ b/src/dev/build/tasks/download_cloud_dependencies.ts @@ -36,12 +36,19 @@ export const DownloadCloudDependencies: Task = { let buildId = ''; if (!config.isRelease) { - const manifest = await Axios.get( - `https://artifacts-api.elastic.co/v1/versions/${config.getBuildVersion()}/builds/latest` - ); - buildId = manifest.data.build.build_id; + const manifestUrl = `https://artifacts-api.elastic.co/v1/versions/${config.getBuildVersion()}/builds/latest`; + try { + const manifest = await Axios.get(manifestUrl); + buildId = manifest.data.build.build_id; + } catch (e) { + log.error( + `Unable to find Elastic artifacts for ${config.getBuildVersion()} at ${manifestUrl}.` + ); + throw e; + } } await del([config.resolveFromRepo('.beats')]); + await downloadBeat('metricbeat', buildId); await downloadBeat('filebeat', buildId); }, From 729b49d113bcbe22c84498e0d093341864ee86e8 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Mon, 7 Feb 2022 16:04:07 -0800 Subject: [PATCH 12/44] [DOCS] Removes Upgrade Assistant doc (#124894) * [DOCS] Removes Upgrade Assistant doc from 8.x * [DOCS] Removes Upgrade Assistant from Stack Management page * Update docs/redirects.asciidoc Co-authored-by: James Rodewig Co-authored-by: James Rodewig --- .../upgrade-assistant/index.asciidoc | 26 ------------------- docs/redirects.asciidoc | 5 ++++ docs/user/management.asciidoc | 6 ----- 3 files changed, 5 insertions(+), 32 deletions(-) delete mode 100644 docs/management/upgrade-assistant/index.asciidoc diff --git a/docs/management/upgrade-assistant/index.asciidoc b/docs/management/upgrade-assistant/index.asciidoc deleted file mode 100644 index ccd3f41b9d886..0000000000000 --- a/docs/management/upgrade-assistant/index.asciidoc +++ /dev/null @@ -1,26 +0,0 @@ -[role="xpack"] -[[upgrade-assistant]] -== Upgrade Assistant - -The Upgrade Assistant helps you prepare for your upgrade -to the next major version of the Elastic Stack. -To access the assistant, open the main menu and go to *Stack Management > Upgrade Assistant*. - -The assistant identifies deprecated settings in your configuration, -enables you to see if you are using deprecated features, -and guides you through the process of resolving issues. - -If you have indices that were created prior to 7.0, -you can use the assistant to reindex them so they can be accessed from 8.0+. - -IMPORTANT: To see the most up-to-date deprecation information before -upgrading to 8.0, upgrade to the latest {prev-major-last} release. - -For more information about upgrading, -refer to {stack-ref}/upgrading-elastic-stack.html[Upgrading to Elastic {version}.] - -[discrete] -=== Required permissions - -The `manage` cluster privilege is required to access the *Upgrade assistant*. -Additional privileges may be needed to perform certain actions. \ No newline at end of file diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 163fed04578ef..0ca518c3a8788 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -395,3 +395,8 @@ This content has moved. Refer to <>. == Upgrade migrations This content has moved. Refer to <>. + +[role="exclude",id="upgrade-assistant"] +== Upgrade Assistant + +This content has moved. Refer to {kibana-ref-all}/7.17/upgrade-assistant.html[Upgrade Assistant]. diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index e682f7372f817..6c309d56f2294 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -167,10 +167,6 @@ set the timespan for notification messages, and much more. the full list of features that are included in your license, see the https://www.elastic.co/subscriptions[subscription page]. -| <> -| Identify the issues that you need to address before upgrading to the -next major version of {es}, and then reindex, if needed. - |=== @@ -197,6 +193,4 @@ include::{kib-repo-dir}/spaces/index.asciidoc[] include::{kib-repo-dir}/management/managing-tags.asciidoc[] -include::{kib-repo-dir}/management/upgrade-assistant/index.asciidoc[] - include::{kib-repo-dir}/management/watcher-ui/index.asciidoc[] From 4394293e217b5c814e64c56c64048debd13f4b6d Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Mon, 7 Feb 2022 19:45:56 -0500 Subject: [PATCH 13/44] [Dashboard] Remove URL Generator (#121832) * Remove deprecated and unused dashboard URL generator code Co-authored-by: Steph Milovic --- .../lib/build_dashboard_container.ts | 2 +- .../get_dashboard_list_item_link.test.ts | 4 +- .../listing/get_dashboard_list_item_link.ts | 7 +- .../dashboard/public/dashboard_constants.ts | 1 + src/plugins/dashboard/public/index.ts | 10 +- src/plugins/dashboard/public/plugin.tsx | 45 +-- .../dashboard/public/services/share.ts | 6 +- .../dashboard/public/url_generator.test.ts | 356 ------------------ src/plugins/dashboard/public/url_generator.ts | 170 --------- .../use_risky_hosts_dashboard_button_href.ts | 12 +- .../use_risky_hosts_dashboard_links.tsx | 54 +-- 11 files changed, 50 insertions(+), 617 deletions(-) delete mode 100644 src/plugins/dashboard/public/url_generator.test.ts delete mode 100644 src/plugins/dashboard/public/url_generator.ts diff --git a/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts b/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts index 1dd39cc3e5ba9..5752a6445d2a9 100644 --- a/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts +++ b/src/plugins/dashboard/public/application/lib/build_dashboard_container.ts @@ -31,7 +31,7 @@ import { } from '../../services/embeddable'; type BuildDashboardContainerProps = DashboardBuildContext & { - data: DashboardAppServices['data']; // the whole data service is required here because it is required by getUrlGeneratorState + data: DashboardAppServices['data']; // the whole data service is required here because it is required by getLocatorParams savedDashboard: DashboardSavedObject; initialDashboardState: DashboardState; incomingEmbeddable?: EmbeddablePackageState; diff --git a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts index a6f80c157bee8..ce9535e549446 100644 --- a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts +++ b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts @@ -9,9 +9,9 @@ import { getDashboardListItemLink } from './get_dashboard_list_item_link'; import { ApplicationStart } from 'kibana/public'; import { createHashHistory } from 'history'; -import { createKbnUrlStateStorage } from '../../../../kibana_utils/public'; -import { GLOBAL_STATE_STORAGE_KEY } from '../../url_generator'; import { FilterStateStore } from '@kbn/es-query'; +import { createKbnUrlStateStorage } from '../../../../kibana_utils/public'; +import { GLOBAL_STATE_STORAGE_KEY } from '../../dashboard_constants'; const DASHBOARD_ID = '13823000-99b9-11ea-9eb6-d9e8adceb647'; diff --git a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts index 2f19924d45982..8af3f2a10666f 100644 --- a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts +++ b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts @@ -9,8 +9,11 @@ import { ApplicationStart } from 'kibana/public'; import { QueryState } from '../../../../data/public'; import { setStateToKbnUrl } from '../../../../kibana_utils/public'; -import { createDashboardEditUrl, DashboardConstants } from '../../dashboard_constants'; -import { GLOBAL_STATE_STORAGE_KEY } from '../../url_generator'; +import { + DashboardConstants, + createDashboardEditUrl, + GLOBAL_STATE_STORAGE_KEY, +} from '../../dashboard_constants'; import { IKbnUrlStateStorage } from '../../services/kibana_utils'; export const getDashboardListItemLink = ( diff --git a/src/plugins/dashboard/public/dashboard_constants.ts b/src/plugins/dashboard/public/dashboard_constants.ts index 9063b279c25f2..88fbc3b30392f 100644 --- a/src/plugins/dashboard/public/dashboard_constants.ts +++ b/src/plugins/dashboard/public/dashboard_constants.ts @@ -9,6 +9,7 @@ import type { ControlStyle } from '../../controls/public'; export const DASHBOARD_STATE_STORAGE_KEY = '_a'; +export const GLOBAL_STATE_STORAGE_KEY = '_g'; export const DashboardConstants = { LANDING_PAGE_PATH: '/list', diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index f25a92275d723..bff2d4d79108c 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -16,15 +16,7 @@ export { } from './application'; export { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; -export type { - DashboardSetup, - DashboardStart, - DashboardUrlGenerator, - DashboardFeatureFlagConfig, -} from './plugin'; - -export type { DashboardUrlGeneratorState } from './url_generator'; -export { DASHBOARD_APP_URL_GENERATOR, createDashboardUrlGenerator } from './url_generator'; +export type { DashboardSetup, DashboardStart, DashboardFeatureFlagConfig } from './plugin'; export type { DashboardAppLocator, DashboardAppLocatorParams } from './locator'; export type { DashboardSavedObject } from './saved_dashboards'; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 6554520fca101..2f63062ccf60c 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -34,7 +34,7 @@ import { PresentationUtilPluginStart } from './services/presentation_util'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from './services/home'; import { NavigationPublicPluginStart as NavigationStart } from './services/navigation'; import { DataPublicPluginSetup, DataPublicPluginStart } from './services/data'; -import { SharePluginSetup, SharePluginStart, UrlGeneratorContract } from './services/share'; +import { SharePluginSetup, SharePluginStart } from './services/share'; import type { SavedObjectTaggingOssPluginStart } from './services/saved_objects_tagging_oss'; import type { ScreenshotModePluginSetup, @@ -70,29 +70,15 @@ import { CopyToDashboardAction, DashboardCapabilities, } from './application'; -import { - createDashboardUrlGenerator, - DASHBOARD_APP_URL_GENERATOR, - DashboardUrlGeneratorState, -} from './url_generator'; import { DashboardAppLocatorDefinition, DashboardAppLocator } from './locator'; import { createSavedDashboardLoader } from './saved_dashboards'; import { DashboardConstants } from './dashboard_constants'; import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; -import { UrlGeneratorState } from '../../share/public'; import { ExportCSVAction } from './application/actions/export_csv_action'; import { dashboardFeatureCatalog } from './dashboard_strings'; import { replaceUrlHashQuery } from '../../kibana_utils/public'; import { SpacesPluginStart } from './services/spaces'; -declare module '../../share/public' { - export interface UrlGeneratorStateMapping { - [DASHBOARD_APP_URL_GENERATOR]: UrlGeneratorState; - } -} - -export type DashboardUrlGenerator = UrlGeneratorContract; - export interface DashboardFeatureFlagConfig { allowByValueEmbeddables: boolean; } @@ -134,15 +120,6 @@ export interface DashboardStart { getDashboardContainerByValueRenderer: () => ReturnType< typeof createDashboardContainerByValueRenderer >; - /** - * @deprecated Use dashboard locator instead. Dashboard locator is available - * under `.locator` key. This dashboard URL generator will be removed soon. - * - * ```ts - * plugins.dashboard.locator.getLocation({ ... }); - * ``` - */ - dashboardUrlGenerator?: DashboardUrlGenerator; locator?: DashboardAppLocator; dashboardFeatureFlagConfig: DashboardFeatureFlagConfig; } @@ -157,11 +134,6 @@ export class DashboardPlugin private stopUrlTracking: (() => void) | undefined = undefined; private currentHistory: ScopedHistory | undefined = undefined; private dashboardFeatureFlagConfig?: DashboardFeatureFlagConfig; - - /** - * @deprecated Use locator instead. - */ - private dashboardUrlGenerator?: DashboardUrlGenerator; private locator?: DashboardAppLocator; public setup( @@ -178,20 +150,6 @@ export class DashboardPlugin ): DashboardSetup { this.dashboardFeatureFlagConfig = this.initializerContext.config.get(); - const startServices = core.getStartServices(); - - if (share) { - this.dashboardUrlGenerator = share.urlGenerators.registerUrlGenerator( - createDashboardUrlGenerator(async () => { - const [coreStart, , selfStart] = await startServices; - return { - appBasePath: coreStart.application.getUrlForApp('dashboards'), - useHashedUrl: coreStart.uiSettings.get('state:storeInSessionStorage'), - savedDashboardLoader: selfStart.getSavedDashboardLoader(), - }; - }) - ); - } const getPlaceholderEmbeddableStartServices = async () => { const [coreStart] = await core.getStartServices(); @@ -458,7 +416,6 @@ export class DashboardPlugin factory: dashboardContainerFactory as DashboardContainerFactory, }); }, - dashboardUrlGenerator: this.dashboardUrlGenerator, locator: this.locator, dashboardFeatureFlagConfig: this.dashboardFeatureFlagConfig!, }; diff --git a/src/plugins/dashboard/public/services/share.ts b/src/plugins/dashboard/public/services/share.ts index 7ed9b86571596..77a9f44a3cf00 100644 --- a/src/plugins/dashboard/public/services/share.ts +++ b/src/plugins/dashboard/public/services/share.ts @@ -6,9 +6,5 @@ * Side Public License, v 1. */ -export type { - SharePluginStart, - SharePluginSetup, - UrlGeneratorContract, -} from '../../../share/public'; +export type { SharePluginStart, SharePluginSetup } from '../../../share/public'; export { downloadMultipleAs } from '../../../share/public'; diff --git a/src/plugins/dashboard/public/url_generator.test.ts b/src/plugins/dashboard/public/url_generator.test.ts deleted file mode 100644 index f1035d7cc1389..0000000000000 --- a/src/plugins/dashboard/public/url_generator.test.ts +++ /dev/null @@ -1,356 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { createDashboardUrlGenerator } from './url_generator'; -import { hashedItemStore } from '../../kibana_utils/public'; -import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; -import { SavedObjectLoader } from '../../saved_objects/public'; -import { type Filter, FilterStateStore } from '@kbn/es-query'; - -const APP_BASE_PATH: string = 'xyz/app/dashboards'; - -const createMockDashboardLoader = ( - dashboardToFilters: { - [dashboardId: string]: () => Filter[]; - } = {} -) => { - return { - get: async (dashboardId: string) => { - return { - searchSource: { - getField: (field: string) => { - if (field === 'filter') - return dashboardToFilters[dashboardId] ? dashboardToFilters[dashboardId]() : []; - throw new Error( - `createMockDashboardLoader > searchSource > getField > ${field} is not mocked` - ); - }, - }, - }; - }, - } as SavedObjectLoader; -}; - -describe('dashboard url generator', () => { - beforeEach(() => { - // @ts-ignore - hashedItemStore.storage = mockStorage; - }); - - test('creates a link to a saved dashboard', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({}); - expect(url).toMatchInlineSnapshot(`"xyz/app/dashboards#/create?_a=()&_g=()"`); - }); - - test('creates a link with global time range set up', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/create?_a=()&_g=(time:(from:now-15m,mode:relative,to:now))"` - ); - }); - - test('creates a link with filters, time range, refresh interval and query to a saved object', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - refreshInterval: { pause: false, value: 300 }, - dashboardId: '123', - filters: [ - { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'hi' }, - }, - { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'hi' }, - $state: { - store: FilterStateStore.GLOBAL_STATE, - }, - }, - ], - query: { query: 'bye', language: 'kuery' }, - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/view/123?_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),query:(language:kuery,query:bye))&_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))"` - ); - }); - - test('searchSessionId', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - refreshInterval: { pause: false, value: 300 }, - dashboardId: '123', - filters: [], - query: { query: 'bye', language: 'kuery' }, - searchSessionId: '__sessionSearchId__', - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/view/123?_a=(filters:!(),query:(language:kuery,query:bye))&_g=(filters:!(),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&searchSessionId=__sessionSearchId__"` - ); - }); - - test('savedQuery', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - savedQuery: '__savedQueryId__', - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/create?_a=(savedQuery:__savedQueryId__)&_g=()"` - ); - expect(url).toContain('__savedQueryId__'); - }); - - test('panels', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - panels: [{ fakePanelContent: 'fakePanelContent' } as any], - }); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/create?_a=(panels:!((fakePanelContent:fakePanelContent)))&_g=()"` - ); - }); - - test('if no useHash setting is given, uses the one was start services', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: true, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - }); - expect(url.indexOf('relative')).toBe(-1); - }); - - test('can override a false useHash ui setting', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - useHash: true, - }); - expect(url.indexOf('relative')).toBe(-1); - }); - - test('can override a true useHash ui setting', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: true, - savedDashboardLoader: createMockDashboardLoader(), - }) - ); - const url = await generator.createUrl!({ - timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, - useHash: false, - }); - expect(url.indexOf('relative')).toBeGreaterThan(1); - }); - - describe('preserving saved filters', () => { - const savedFilter1 = { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'savedfilter1' }, - }; - - const savedFilter2 = { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'savedfilter2' }, - }; - - const appliedFilter = { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { query: 'appliedfilter' }, - }; - - test('attaches filters from destination dashboard', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => [savedFilter1], - ['dashboard2']: () => [savedFilter2], - }), - }) - ); - - const urlToDashboard1 = await generator.createUrl!({ - dashboardId: 'dashboard1', - filters: [appliedFilter], - }); - - expect(urlToDashboard1).toEqual(expect.stringContaining('query:savedfilter1')); - expect(urlToDashboard1).toEqual(expect.stringContaining('query:appliedfilter')); - - const urlToDashboard2 = await generator.createUrl!({ - dashboardId: 'dashboard2', - filters: [appliedFilter], - }); - - expect(urlToDashboard2).toEqual(expect.stringContaining('query:savedfilter2')); - expect(urlToDashboard2).toEqual(expect.stringContaining('query:appliedfilter')); - }); - - test("doesn't fail if can't retrieve filters from destination dashboard", async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => { - throw new Error('Not found'); - }, - }), - }) - ); - - const url = await generator.createUrl!({ - dashboardId: 'dashboard1', - filters: [appliedFilter], - }); - - expect(url).not.toEqual(expect.stringContaining('query:savedfilter1')); - expect(url).toEqual(expect.stringContaining('query:appliedfilter')); - }); - - test('can enforce empty filters', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => [savedFilter1], - }), - }) - ); - - const url = await generator.createUrl!({ - dashboardId: 'dashboard1', - filters: [], - preserveSavedFilters: false, - }); - - expect(url).not.toEqual(expect.stringContaining('query:savedfilter1')); - expect(url).not.toEqual(expect.stringContaining('query:appliedfilter')); - expect(url).toMatchInlineSnapshot( - `"xyz/app/dashboards#/view/dashboard1?_a=(filters:!())&_g=(filters:!())"` - ); - }); - - test('no filters in result url if no filters applied', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => [savedFilter1], - }), - }) - ); - - const url = await generator.createUrl!({ - dashboardId: 'dashboard1', - }); - expect(url).not.toEqual(expect.stringContaining('filters')); - expect(url).toMatchInlineSnapshot(`"xyz/app/dashboards#/view/dashboard1?_a=()&_g=()"`); - }); - - test('can turn off preserving filters', async () => { - const generator = createDashboardUrlGenerator(() => - Promise.resolve({ - appBasePath: APP_BASE_PATH, - useHashedUrl: false, - savedDashboardLoader: createMockDashboardLoader({ - ['dashboard1']: () => [savedFilter1], - }), - }) - ); - const urlWithPreservedFiltersTurnedOff = await generator.createUrl!({ - dashboardId: 'dashboard1', - filters: [appliedFilter], - preserveSavedFilters: false, - }); - - expect(urlWithPreservedFiltersTurnedOff).not.toEqual( - expect.stringContaining('query:savedfilter1') - ); - expect(urlWithPreservedFiltersTurnedOff).toEqual( - expect.stringContaining('query:appliedfilter') - ); - }); - }); -}); diff --git a/src/plugins/dashboard/public/url_generator.ts b/src/plugins/dashboard/public/url_generator.ts deleted file mode 100644 index 5c0cd32ee5a16..0000000000000 --- a/src/plugins/dashboard/public/url_generator.ts +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - TimeRange, - Filter, - Query, - esFilters, - QueryState, - RefreshInterval, -} from '../../data/public'; -import { setStateToKbnUrl } from '../../kibana_utils/public'; -import { UrlGeneratorsDefinition } from '../../share/public'; -import { SavedObjectLoader } from '../../saved_objects/public'; -import { ViewMode } from '../../embeddable/public'; -import { DashboardConstants } from './dashboard_constants'; -import { SavedDashboardPanel } from '../common/types'; - -export const STATE_STORAGE_KEY = '_a'; -export const GLOBAL_STATE_STORAGE_KEY = '_g'; - -export const DASHBOARD_APP_URL_GENERATOR = 'DASHBOARD_APP_URL_GENERATOR'; - -/** - * @deprecated Use dashboard locator instead. - */ -export interface DashboardUrlGeneratorState { - /** - * If given, the dashboard saved object with this id will be loaded. If not given, - * a new, unsaved dashboard will be loaded up. - */ - dashboardId?: string; - /** - * Optionally set the time range in the time picker. - */ - timeRange?: TimeRange; - - /** - * Optionally set the refresh interval. - */ - refreshInterval?: RefreshInterval; - - /** - * Optionally apply filers. NOTE: if given and used in conjunction with `dashboardId`, and the - * saved dashboard has filters saved with it, this will _replace_ those filters. - */ - filters?: Filter[]; - /** - * Optionally set a query. NOTE: if given and used in conjunction with `dashboardId`, and the - * saved dashboard has a query saved with it, this will _replace_ that query. - */ - query?: Query; - /** - * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines - * whether to hash the data in the url to avoid url length issues. - */ - useHash?: boolean; - - /** - * When `true` filters from saved filters from destination dashboard as merged with applied filters - * When `false` applied filters take precedence and override saved filters - * - * true is default - */ - preserveSavedFilters?: boolean; - - /** - * View mode of the dashboard. - */ - viewMode?: ViewMode; - - /** - * Search search session ID to restore. - * (Background search) - */ - searchSessionId?: string; - - /** - * List of dashboard panels - */ - panels?: SavedDashboardPanel[]; - - /** - * Saved query ID - */ - savedQuery?: string; -} - -/** - * @deprecated Use dashboard locator instead. - */ -export const createDashboardUrlGenerator = ( - getStartServices: () => Promise<{ - appBasePath: string; - useHashedUrl: boolean; - savedDashboardLoader: SavedObjectLoader; - }> -): UrlGeneratorsDefinition => ({ - id: DASHBOARD_APP_URL_GENERATOR, - createUrl: async (state) => { - const startServices = await getStartServices(); - const useHash = state.useHash ?? startServices.useHashedUrl; - const appBasePath = startServices.appBasePath; - const hash = state.dashboardId ? `view/${state.dashboardId}` : `create`; - - const getSavedFiltersFromDestinationDashboardIfNeeded = async (): Promise => { - if (state.preserveSavedFilters === false) return []; - if (!state.dashboardId) return []; - try { - const dashboard = await startServices.savedDashboardLoader.get(state.dashboardId); - return dashboard?.searchSource?.getField('filter') ?? []; - } catch (e) { - // in case dashboard is missing, built the url without those filters - // dashboard app will handle redirect to landing page with toast message - return []; - } - }; - - const cleanEmptyKeys = (stateObj: Record) => { - Object.keys(stateObj).forEach((key) => { - if (stateObj[key] === undefined) { - delete stateObj[key]; - } - }); - return stateObj; - }; - - // leave filters `undefined` if no filters was applied - // in this case dashboard will restore saved filters on its own - const filters = state.filters && [ - ...(await getSavedFiltersFromDestinationDashboardIfNeeded()), - ...state.filters, - ]; - - let url = setStateToKbnUrl( - STATE_STORAGE_KEY, - cleanEmptyKeys({ - query: state.query, - filters: filters?.filter((f) => !esFilters.isFilterPinned(f)), - viewMode: state.viewMode, - panels: state.panels, - savedQuery: state.savedQuery, - }), - { useHash }, - `${appBasePath}#/${hash}` - ); - - url = setStateToKbnUrl( - GLOBAL_STATE_STORAGE_KEY, - cleanEmptyKeys({ - time: state.timeRange, - filters: filters?.filter((f) => esFilters.isFilterPinned(f)), - refreshInterval: state.refreshInterval, - }), - { useHash }, - url - ); - - if (state.searchSessionId) { - url = `${url}&${DashboardConstants.SEARCH_SESSION_ID}=${state.searchSessionId}`; - } - - return url; - }, -}); diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts index 555ae7544180b..5bc2087dc63ab 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts @@ -16,13 +16,15 @@ export const DASHBOARD_REQUEST_BODY = { }; export const useRiskyHostsDashboardButtonHref = (to: string, from: string) => { - const createDashboardUrl = useKibana().services.dashboard?.dashboardUrlGenerator?.createUrl; - const savedObjectsClient = useKibana().services.savedObjects.client; + const { + dashboard, + savedObjects: { client: savedObjectsClient }, + } = useKibana().services; const [buttonHref, setButtonHref] = useState(); useEffect(() => { - if (createDashboardUrl && savedObjectsClient) { + if (dashboard?.locator && savedObjectsClient) { savedObjectsClient.find(DASHBOARD_REQUEST_BODY).then( async (DashboardsSO?: { savedObjects?: Array<{ @@ -31,7 +33,7 @@ export const useRiskyHostsDashboardButtonHref = (to: string, from: string) => { }>; }) => { if (DashboardsSO?.savedObjects?.length) { - const dashboardUrl = await createDashboardUrl({ + const dashboardUrl = await dashboard?.locator?.getUrl({ dashboardId: DashboardsSO.savedObjects[0].id, timeRange: { to, @@ -43,7 +45,7 @@ export const useRiskyHostsDashboardButtonHref = (to: string, from: string) => { } ); } - }, [createDashboardUrl, from, savedObjectsClient, to]); + }, [dashboard, from, savedObjectsClient, to]); return { buttonHref, diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx index 002dc18227f6d..5b8bf180da1f8 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx @@ -14,40 +14,48 @@ export const useRiskyHostsDashboardLinks = ( from: string, listItems: LinkPanelListItem[] ) => { - const createDashboardUrl = useKibana().services.dashboard?.locator?.getLocation; + const { dashboard } = useKibana().services; + const dashboardId = useRiskyHostsDashboardId(); const [listItemsWithLinks, setListItemsWithLinks] = useState([]); useEffect(() => { let cancelled = false; const createLinks = async () => { - if (createDashboardUrl && dashboardId) { + if (dashboard?.locator && dashboardId) { const dashboardUrls = await Promise.all( - listItems.map((listItem) => - createDashboardUrl({ - dashboardId, - timeRange: { - to, - from, - }, - filters: [ - { - meta: { - alias: null, - disabled: false, - negate: false, - }, - query: { match_phrase: { 'host.name': listItem.title } }, - }, - ], - }) + listItems.reduce( + (acc: Array>, listItem) => + dashboard && dashboard.locator + ? [ + ...acc, + dashboard.locator.getUrl({ + dashboardId, + timeRange: { + to, + from, + }, + filters: [ + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { match_phrase: { 'host.name': listItem.title } }, + }, + ], + }), + ] + : acc, + [] ) ); - if (!cancelled) { + if (!cancelled && dashboardUrls.length) { setListItemsWithLinks( listItems.map((item, i) => ({ ...item, - path: dashboardUrls[i] as unknown as string, + path: dashboardUrls[i], })) ); } @@ -59,7 +67,7 @@ export const useRiskyHostsDashboardLinks = ( return () => { cancelled = true; }; - }, [createDashboardUrl, dashboardId, from, listItems, to]); + }, [dashboard, dashboardId, from, listItems, to]); return { listItemsWithLinks }; }; From 97230e94310ceaa0f364f43ed577efadd34c9766 Mon Sep 17 00:00:00 2001 From: Byron Hulcher Date: Mon, 7 Feb 2022 19:51:49 -0500 Subject: [PATCH 14/44] [App Search] New modal to crawl select domains (#124195) --- .../crawl_select_domains_modal.scss | 4 + .../crawl_select_domains_modal.test.tsx | 98 +++++++++++++++ .../crawl_select_domains_modal.tsx | 109 ++++++++++++++++ .../crawl_select_domains_modal_logic.test.ts | 100 +++++++++++++++ .../crawl_select_domains_modal_logic.ts | 67 ++++++++++ .../simplified_selectable.test.tsx | 118 ++++++++++++++++++ .../simplified_selectable.tsx | 90 +++++++++++++ .../crawler_status_indicator.test.tsx | 9 +- .../crawler_status_indicator.tsx | 20 +-- .../start_crawl_context_menu.test.tsx | 76 +++++++++++ .../start_crawl_context_menu.tsx | 79 ++++++++++++ .../components/crawler/crawler_logic.test.ts | 40 +++++- .../components/crawler/crawler_logic.ts | 14 ++- .../crawler/crawler_overview.test.tsx | 7 ++ .../components/crawler/crawler_overview.tsx | 2 + .../crawler/crawler_single_domain.test.tsx | 7 ++ .../crawler/crawler_single_domain.tsx | 2 + .../server/routes/app_search/crawler.test.ts | 13 ++ .../server/routes/app_search/crawler.ts | 7 ++ 19 files changed, 840 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.scss new file mode 100644 index 0000000000000..09abf97829be4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.scss @@ -0,0 +1,4 @@ +.crawlSelectDomainsModal { + width: 50rem; + max-width: 90%; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.test.tsx new file mode 100644 index 0000000000000..79898d9f15e9d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.test.tsx @@ -0,0 +1,98 @@ +/* + * 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 { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiModal, EuiModalFooter, EuiButton, EuiButtonEmpty } from '@elastic/eui'; + +import { rerender } from '../../../../../test_helpers'; + +import { CrawlSelectDomainsModal } from './crawl_select_domains_modal'; +import { SimplifiedSelectable } from './simplified_selectable'; + +const MOCK_VALUES = { + // CrawlerLogic + domains: [{ url: 'https://www.elastic.co' }, { url: 'https://www.swiftype.com' }], + // CrawlSelectDomainsModalLogic + selectedDomainUrls: ['https://www.elastic.co'], + isModalVisible: true, +}; + +const MOCK_ACTIONS = { + // CrawlSelectDomainsModalLogic + hideModal: jest.fn(), + onSelectDomainUrls: jest.fn(), + // CrawlerLogic + startCrawl: jest.fn(), +}; + +describe('CrawlSelectDomainsModal', () => { + let wrapper: ShallowWrapper; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(MOCK_VALUES); + setMockActions(MOCK_ACTIONS); + + wrapper = shallow(); + }); + + it('is empty when the modal is hidden', () => { + setMockValues({ + ...MOCK_VALUES, + isModalVisible: false, + }); + + rerender(wrapper); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('renders as a modal when visible', () => { + expect(wrapper.is(EuiModal)).toBe(true); + }); + + it('can be closed', () => { + expect(wrapper.prop('onClose')).toEqual(MOCK_ACTIONS.hideModal); + expect(wrapper.find(EuiModalFooter).find(EuiButtonEmpty).prop('onClick')).toEqual( + MOCK_ACTIONS.hideModal + ); + }); + + it('allows the user to select domains', () => { + expect(wrapper.find(SimplifiedSelectable).props()).toEqual({ + options: ['https://www.elastic.co', 'https://www.swiftype.com'], + selectedOptions: ['https://www.elastic.co'], + onChange: MOCK_ACTIONS.onSelectDomainUrls, + }); + }); + + describe('submit button', () => { + it('is disabled when no domains are selected', () => { + setMockValues({ + ...MOCK_VALUES, + selectedDomainUrls: [], + }); + + rerender(wrapper); + + expect(wrapper.find(EuiModalFooter).find(EuiButton).prop('disabled')).toEqual(true); + }); + + it('starts a crawl and hides the modal', () => { + wrapper.find(EuiModalFooter).find(EuiButton).simulate('click'); + + expect(MOCK_ACTIONS.startCrawl).toHaveBeenCalledWith({ + domain_allowlist: MOCK_VALUES.selectedDomainUrls, + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.tsx new file mode 100644 index 0000000000000..211266a779df9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal.tsx @@ -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 React from 'react'; + +import { useValues, useActions } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiNotificationBadge, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { CANCEL_BUTTON_LABEL } from '../../../../../shared/constants'; + +import { CrawlerLogic } from '../../crawler_logic'; + +import { CrawlSelectDomainsModalLogic } from './crawl_select_domains_modal_logic'; +import { SimplifiedSelectable } from './simplified_selectable'; + +import './crawl_select_domains_modal.scss'; + +export const CrawlSelectDomainsModal: React.FC = () => { + const { domains } = useValues(CrawlerLogic); + const domainUrls = domains.map((domain) => domain.url); + + const crawlSelectDomainsModalLogic = CrawlSelectDomainsModalLogic({ domains }); + const { isDataLoading, isModalVisible, selectedDomainUrls } = useValues( + crawlSelectDomainsModalLogic + ); + const { hideModal, onSelectDomainUrls } = useActions(crawlSelectDomainsModalLogic); + + const { startCrawl } = useActions(CrawlerLogic); + + if (!isModalVisible) { + return null; + } + + return ( + + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlSelectDomainsModal.modalHeaderTitle', + { + defaultMessage: 'Crawl select domains', + } + )} + + + 0 ? 'accent' : 'subdued'} + > + {selectedDomainUrls.length} + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlSelectDomainsModal.selectedDescriptor', + { + defaultMessage: 'selected', + } + )} + + + + + + + + {CANCEL_BUTTON_LABEL} + { + startCrawl({ domain_allowlist: selectedDomainUrls }); + }} + disabled={selectedDomainUrls.length === 0} + isLoading={isDataLoading} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlSelectDomainsModal.startCrawlButtonLabel', + { + defaultMessage: 'Apply and crawl now', + } + )} + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.test.ts new file mode 100644 index 0000000000000..ef6ef4d09fadb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.test.ts @@ -0,0 +1,100 @@ +/* + * 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 { LogicMounter } from '../../../../../__mocks__/kea_logic'; + +import { CrawlerLogic } from '../../crawler_logic'; + +import { CrawlSelectDomainsModalLogic } from './crawl_select_domains_modal_logic'; + +describe('CrawlSelectDomainsModalLogic', () => { + const { mount } = new LogicMounter(CrawlSelectDomainsModalLogic); + + beforeEach(() => { + jest.clearAllMocks(); + mount(); + }); + + it('has expected default values', () => { + expect(CrawlSelectDomainsModalLogic.values).toEqual({ + isDataLoading: false, + isModalVisible: false, + selectedDomainUrls: [], + }); + }); + + describe('actions', () => { + describe('hideModal', () => { + it('hides the modal', () => { + CrawlSelectDomainsModalLogic.actions.hideModal(); + + expect(CrawlSelectDomainsModalLogic.values.isModalVisible).toBe(false); + }); + }); + + describe('showModal', () => { + it('shows the modal', () => { + CrawlSelectDomainsModalLogic.actions.showModal(); + + expect(CrawlSelectDomainsModalLogic.values.isModalVisible).toBe(true); + }); + + it('resets the selected options', () => { + mount({ + selectedDomainUrls: ['https://www.elastic.co', 'https://www.swiftype.com'], + }); + + CrawlSelectDomainsModalLogic.actions.showModal(); + + expect(CrawlSelectDomainsModalLogic.values.selectedDomainUrls).toEqual([]); + }); + }); + + describe('onSelectDomainUrls', () => { + it('saves the urls', () => { + mount({ + selectedDomainUrls: [], + }); + + CrawlSelectDomainsModalLogic.actions.onSelectDomainUrls([ + 'https://www.elastic.co', + 'https://www.swiftype.com', + ]); + + expect(CrawlSelectDomainsModalLogic.values.selectedDomainUrls).toEqual([ + 'https://www.elastic.co', + 'https://www.swiftype.com', + ]); + }); + }); + + describe('[CrawlerLogic.actionTypes.startCrawl]', () => { + it('enables loading state', () => { + mount({ + isDataLoading: false, + }); + + CrawlerLogic.actions.startCrawl(); + + expect(CrawlSelectDomainsModalLogic.values.isDataLoading).toBe(true); + }); + }); + + describe('[CrawlerLogic.actionTypes.onStartCrawlRequestComplete]', () => { + it('disables loading state and hides the modal', () => { + mount({ + isDataLoading: true, + isModalVisible: true, + }); + + CrawlerLogic.actions.onStartCrawlRequestComplete(); + + expect(CrawlSelectDomainsModalLogic.values.isDataLoading).toBe(false); + expect(CrawlSelectDomainsModalLogic.values.isModalVisible).toBe(false); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.ts new file mode 100644 index 0000000000000..088950cbffd3f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/crawl_select_domains_modal_logic.ts @@ -0,0 +1,67 @@ +/* + * 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 { kea, MakeLogicType } from 'kea'; + +import { CrawlerLogic } from '../../crawler_logic'; + +import { CrawlerDomain } from '../../types'; + +export interface CrawlSelectDomainsLogicProps { + domains: CrawlerDomain[]; +} + +export interface CrawlSelectDomainsLogicValues { + isDataLoading: boolean; + isModalVisible: boolean; + selectedDomainUrls: string[]; +} + +export interface CrawlSelectDomainsModalLogicActions { + hideModal(): void; + onSelectDomainUrls(domainUrls: string[]): { domainUrls: string[] }; + showModal(): void; +} + +export const CrawlSelectDomainsModalLogic = kea< + MakeLogicType< + CrawlSelectDomainsLogicValues, + CrawlSelectDomainsModalLogicActions, + CrawlSelectDomainsLogicProps + > +>({ + path: ['enterprise_search', 'app_search', 'crawler', 'crawl_select_domains_modal'], + actions: () => ({ + hideModal: true, + onSelectDomainUrls: (domainUrls) => ({ domainUrls }), + showModal: true, + }), + reducers: () => ({ + isDataLoading: [ + false, + { + [CrawlerLogic.actionTypes.startCrawl]: () => true, + [CrawlerLogic.actionTypes.onStartCrawlRequestComplete]: () => false, + }, + ], + isModalVisible: [ + false, + { + showModal: () => true, + hideModal: () => false, + [CrawlerLogic.actionTypes.onStartCrawlRequestComplete]: () => false, + }, + ], + selectedDomainUrls: [ + [], + { + showModal: () => [], + onSelectDomainUrls: (_, { domainUrls }) => domainUrls, + }, + ], + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.test.tsx new file mode 100644 index 0000000000000..a90259f8dac3c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.test.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiSelectable, EuiSelectableList, EuiSelectableSearch } from '@elastic/eui'; + +import { mountWithIntl } from '../../../../../test_helpers'; + +import { SimplifiedSelectable } from './simplified_selectable'; + +describe('SimplifiedSelectable', () => { + let wrapper: ShallowWrapper; + + const MOCK_ON_CHANGE = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + wrapper = shallow( + + ); + }); + + it('combines the options and selected options', () => { + expect(wrapper.find(EuiSelectable).prop('options')).toEqual([ + { + label: 'cat', + checked: 'on', + }, + { + label: 'dog', + }, + { + label: 'fish', + checked: 'on', + }, + ]); + }); + + it('passes newly selected options to the callback', () => { + wrapper.find(EuiSelectable).simulate('change', [ + { + label: 'cat', + checked: 'on', + }, + { + label: 'dog', + }, + { + label: 'fish', + checked: 'on', + }, + ]); + + expect(MOCK_ON_CHANGE).toHaveBeenCalledWith(['cat', 'fish']); + }); + + describe('select all button', () => { + it('it is disabled when all options are already selected', () => { + wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="SelectAllButton"]').prop('disabled')).toEqual(true); + }); + + it('allows the user to select all options', () => { + wrapper.find('[data-test-subj="SelectAllButton"]').simulate('click'); + expect(MOCK_ON_CHANGE).toHaveBeenLastCalledWith(['cat', 'dog', 'fish']); + }); + }); + + describe('deselect all button', () => { + it('it is disabled when all no options are selected', () => { + wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="DeselectAllButton"]').prop('disabled')).toEqual(true); + }); + + it('allows the user to select all options', () => { + wrapper.find('[data-test-subj="DeselectAllButton"]').simulate('click'); + expect(MOCK_ON_CHANGE).toHaveBeenLastCalledWith([]); + }); + }); + + it('renders a search bar and selectable list', () => { + const fullRender = mountWithIntl( + + ); + + expect(fullRender.find(EuiSelectableSearch)).toHaveLength(1); + expect(fullRender.find(EuiSelectableList)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.tsx new file mode 100644 index 0000000000000..07ede1c59971a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_select_domains_modal/simplified_selectable.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; + +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSelectable } from '@elastic/eui'; +import { EuiSelectableLIOption } from '@elastic/eui/src/components/selectable/selectable_option'; +import { i18n } from '@kbn/i18n'; + +export interface Props { + options: string[]; + selectedOptions: string[]; + onChange(selectedOptions: string[]): void; +} + +export interface OptionMap { + [key: string]: boolean; +} + +export const SimplifiedSelectable: React.FC = ({ options, selectedOptions, onChange }) => { + const selectedOptionsMap: OptionMap = selectedOptions.reduce( + (acc, selectedOption) => ({ + ...acc, + [selectedOption]: true, + }), + {} + ); + + const selectableOptions: Array> = options.map((option) => ({ + label: option, + checked: selectedOptionsMap[option] ? 'on' : undefined, + })); + + return ( + <> + + + onChange(options)} + disabled={selectedOptions.length === options.length} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.simplifiedSelectable.selectAllButtonLabel', + { + defaultMessage: 'Select all', + } + )} + + + + onChange([])} + disabled={selectedOptions.length === 0} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.simplifiedSelectable.deselectAllButtonLabel', + { + defaultMessage: 'Deselect all', + } + )} + + + + { + onChange( + newSelectableOptions.filter((option) => option.checked).map((option) => option.label) + ); + }} + > + {(list, search) => ( + <> + {search} + {list} + + )} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx index c46c360934d0b..cc8b1891838b3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx @@ -16,6 +16,7 @@ import { EuiButton } from '@elastic/eui'; import { CrawlerDomain, CrawlerStatus } from '../../types'; import { CrawlerStatusIndicator } from './crawler_status_indicator'; +import { StartCrawlContextMenu } from './start_crawl_context_menu'; import { StopCrawlPopoverContextMenu } from './stop_crawl_popover_context_menu'; const MOCK_VALUES = { @@ -72,9 +73,7 @@ describe('CrawlerStatusIndicator', () => { }); const wrapper = shallow(); - expect(wrapper.is(EuiButton)).toEqual(true); - expect(wrapper.render().text()).toContain('Start a crawl'); - expect(wrapper.prop('onClick')).toEqual(MOCK_ACTIONS.startCrawl); + expect(wrapper.is(StartCrawlContextMenu)).toEqual(true); }); }); @@ -87,9 +86,7 @@ describe('CrawlerStatusIndicator', () => { }); const wrapper = shallow(); - expect(wrapper.is(EuiButton)).toEqual(true); - expect(wrapper.render().text()).toContain('Retry crawl'); - expect(wrapper.prop('onClick')).toEqual(MOCK_ACTIONS.startCrawl); + expect(wrapper.is(StartCrawlContextMenu)).toEqual(true); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx index c02e45f02c407..d750cf100202f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx @@ -16,14 +16,15 @@ import { i18n } from '@kbn/i18n'; import { CrawlerLogic } from '../../crawler_logic'; import { CrawlerStatus } from '../../types'; +import { StartCrawlContextMenu } from './start_crawl_context_menu'; import { StopCrawlPopoverContextMenu } from './stop_crawl_popover_context_menu'; export const CrawlerStatusIndicator: React.FC = () => { const { domains, mostRecentCrawlRequestStatus } = useValues(CrawlerLogic); - const { startCrawl, stopCrawl } = useActions(CrawlerLogic); + const { stopCrawl } = useActions(CrawlerLogic); const disabledButton = ( - + {i18n.translate( 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusIndicator.startACrawlButtonLabel', { @@ -40,26 +41,27 @@ export const CrawlerStatusIndicator: React.FC = () => { switch (mostRecentCrawlRequestStatus) { case CrawlerStatus.Success: return ( - - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusIndicator.startACrawlButtonLabel', + + /> ); case CrawlerStatus.Failed: case CrawlerStatus.Canceled: return ( - - {i18n.translate( + + /> ); case CrawlerStatus.Pending: case CrawlerStatus.Suspended: diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.test.tsx new file mode 100644 index 0000000000000..6d9f1cd7be64b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.test.tsx @@ -0,0 +1,76 @@ +/* + * 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 { setMockActions } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { ReactWrapper, shallow } from 'enzyme'; + +import { + EuiButton, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiPopover, + EuiResizeObserver, +} from '@elastic/eui'; + +import { mountWithIntl } from '../../../../../test_helpers'; + +import { StartCrawlContextMenu } from './start_crawl_context_menu'; + +const MOCK_ACTIONS = { + startCrawl: jest.fn(), + showModal: jest.fn(), +}; + +describe('StartCrawlContextMenu', () => { + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(MOCK_ACTIONS); + }); + + it('is initially closed', () => { + const wrapper = shallow(); + + expect(wrapper.is(EuiPopover)).toBe(true); + expect(wrapper.prop('isOpen')).toEqual(false); + }); + + describe('user actions', () => { + let wrapper: ReactWrapper; + let menuItems: ReactWrapper; + + beforeEach(() => { + wrapper = mountWithIntl(); + + wrapper.find(EuiButton).simulate('click'); + + menuItems = wrapper + .find(EuiContextMenuPanel) + .find(EuiResizeObserver) + .find(EuiContextMenuItem); + }); + + it('can be opened', () => { + expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(true); + expect(menuItems.length).toEqual(2); + }); + + it('can start crawls', () => { + menuItems.at(0).simulate('click'); + + expect(MOCK_ACTIONS.startCrawl).toHaveBeenCalled(); + }); + + it('can open a modal to start a crawl with selected domains', () => { + menuItems.at(1).simulate('click'); + + expect(MOCK_ACTIONS.showModal).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.tsx new file mode 100644 index 0000000000000..1182a845bd4f7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/start_crawl_context_menu.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState } from 'react'; + +import { useActions } from 'kea'; + +import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { CrawlerLogic } from '../../crawler_logic'; + +import { CrawlSelectDomainsModalLogic } from '../crawl_select_domains_modal/crawl_select_domains_modal_logic'; + +interface Props { + menuButtonLabel?: string; + fill?: boolean; +} + +export const StartCrawlContextMenu: React.FC = ({ menuButtonLabel, fill }) => { + const { startCrawl } = useActions(CrawlerLogic); + const { showModal: showCrawlSelectDomainsModal } = useActions(CrawlSelectDomainsModalLogic); + + const [isPopoverOpen, setPopover] = useState(false); + + const togglePopover = () => setPopover(!isPopoverOpen); + + const closePopover = () => setPopover(false); + + return ( + + {menuButtonLabel} + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + { + closePopover(); + startCrawl(); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.startCrawlContextMenu.crawlAllDomainsMenuLabel', + { + defaultMessage: 'Crawl all domains on this engine', + } + )} + , + { + closePopover(); + showCrawlSelectDomainsModal(); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.startCrawlContextMenu.crawlSelectDomainsMenuLabel', + { + defaultMessage: 'Crawl select domains', + } + )} + , + ]} + /> + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts index e622798e688ab..59ec64c69d5a8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts @@ -226,7 +226,7 @@ describe('CrawlerLogic', () => { CrawlerStatus.Running, CrawlerStatus.Canceling, ].forEach((status) => { - it(`creates a new timeout for status ${status}`, async () => { + it(`creates a new timeout for most recent crawl request status ${status}`, async () => { jest.spyOn(CrawlerLogic.actions, 'createNewTimeoutForCrawlerData'); http.get.mockReturnValueOnce( Promise.resolve({ @@ -260,6 +260,27 @@ describe('CrawlerLogic', () => { expect(CrawlerLogic.actions.fetchCrawlerData).toHaveBeenCalled(); }); }); + + it('clears the timeout if no events are active', async () => { + jest.spyOn(CrawlerLogic.actions, 'clearTimeoutId'); + + http.get.mockReturnValueOnce( + Promise.resolve({ + ...MOCK_SERVER_CRAWLER_DATA, + events: [ + { + status: CrawlerStatus.Failed, + crawl_config: {}, + }, + ], + }) + ); + + CrawlerLogic.actions.fetchCrawlerData(); + await nextTick(); + + expect(CrawlerLogic.actions.clearTimeoutId).toHaveBeenCalled(); + }); }); it('calls flashApiErrors when there is an error on the request for crawler data', async () => { @@ -276,23 +297,36 @@ describe('CrawlerLogic', () => { describe('startCrawl', () => { describe('success path', () => { - it('creates a new crawl request and then fetches the latest crawler data', async () => { + it('creates a new crawl request, fetches latest crawler data, then marks the request complete', async () => { jest.spyOn(CrawlerLogic.actions, 'fetchCrawlerData'); + jest.spyOn(CrawlerLogic.actions, 'onStartCrawlRequestComplete'); http.post.mockReturnValueOnce(Promise.resolve()); CrawlerLogic.actions.startCrawl(); await nextTick(); expect(http.post).toHaveBeenCalledWith( - '/internal/app_search/engines/some-engine/crawler/crawl_requests' + '/internal/app_search/engines/some-engine/crawler/crawl_requests', + { body: JSON.stringify({ overrides: {} }) } ); expect(CrawlerLogic.actions.fetchCrawlerData).toHaveBeenCalled(); + expect(CrawlerLogic.actions.onStartCrawlRequestComplete).toHaveBeenCalled(); }); }); itShowsServerErrorAsFlashMessage(http.post, () => { CrawlerLogic.actions.startCrawl(); }); + + it('marks the request complete even after an error', async () => { + jest.spyOn(CrawlerLogic.actions, 'onStartCrawlRequestComplete'); + http.post.mockReturnValueOnce(Promise.reject()); + + CrawlerLogic.actions.startCrawl(); + await nextTick(); + + expect(CrawlerLogic.actions.onStartCrawlRequestComplete).toHaveBeenCalled(); + }); }); describe('stopCrawl', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts index 08a01af67ece6..d68dbc59f06d0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts @@ -48,7 +48,8 @@ interface CrawlerActions { fetchCrawlerData(): void; onCreateNewTimeout(timeoutId: NodeJS.Timeout): { timeoutId: NodeJS.Timeout }; onReceiveCrawlerData(data: CrawlerData): { data: CrawlerData }; - startCrawl(): void; + onStartCrawlRequestComplete(): void; + startCrawl(overrides?: object): { overrides?: object }; stopCrawl(): void; } @@ -60,7 +61,8 @@ export const CrawlerLogic = kea>({ fetchCrawlerData: true, onCreateNewTimeout: (timeoutId) => ({ timeoutId }), onReceiveCrawlerData: (data) => ({ data }), - startCrawl: () => null, + onStartCrawlRequestComplete: true, + startCrawl: (overrides) => ({ overrides }), stopCrawl: () => null, }, reducers: { @@ -135,15 +137,19 @@ export const CrawlerLogic = kea>({ actions.createNewTimeoutForCrawlerData(POLLING_DURATION_ON_FAILURE); } }, - startCrawl: async () => { + startCrawl: async ({ overrides = {} }) => { const { http } = HttpLogic.values; const { engineName } = EngineLogic.values; try { - await http.post(`/internal/app_search/engines/${engineName}/crawler/crawl_requests`); + await http.post(`/internal/app_search/engines/${engineName}/crawler/crawl_requests`, { + body: JSON.stringify({ overrides }), + }); actions.fetchCrawlerData(); } catch (e) { flashAPIErrors(e); + } finally { + actions.onStartCrawlRequestComplete(); } }, stopCrawl: async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx index 4d72b854bddfb..509346542ae13 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx @@ -23,6 +23,7 @@ import { AddDomainFormSubmitButton } from './components/add_domain/add_domain_fo import { AddDomainLogic } from './components/add_domain/add_domain_logic'; import { CrawlDetailsFlyout } from './components/crawl_details_flyout'; import { CrawlRequestsTable } from './components/crawl_requests_table'; +import { CrawlSelectDomainsModal } from './components/crawl_select_domains_modal/crawl_select_domains_modal'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DomainsTable } from './components/domains_table'; @@ -215,4 +216,10 @@ describe('CrawlerOverview', () => { expect(wrapper.find(AddDomainFormErrors)).toHaveLength(1); }); + + it('contains a modal to start a crawl with selected domains', () => { + const wrapper = shallow(); + + expect(wrapper.find(CrawlSelectDomainsModal)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx index c68e75790f073..f1f25dfb4dc55 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx @@ -24,6 +24,7 @@ import { AddDomainFormSubmitButton } from './components/add_domain/add_domain_fo import { AddDomainLogic } from './components/add_domain/add_domain_logic'; import { CrawlDetailsFlyout } from './components/crawl_details_flyout'; import { CrawlRequestsTable } from './components/crawl_requests_table'; +import { CrawlSelectDomainsModal } from './components/crawl_select_domains_modal/crawl_select_domains_modal'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DomainsTable } from './components/domains_table'; @@ -138,6 +139,7 @@ export const CrawlerOverview: React.FC = () => { )} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx index ed445b923ea2a..addf4093a167b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx @@ -15,6 +15,7 @@ import { shallow } from 'enzyme'; import { getPageHeaderActions } from '../../../test_helpers'; +import { CrawlSelectDomainsModal } from './components/crawl_select_domains_modal/crawl_select_domains_modal'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DeduplicationPanel } from './components/deduplication_panel'; @@ -92,4 +93,10 @@ describe('CrawlerSingleDomain', () => { expect(wrapper.find(DeleteDomainPanel)).toHaveLength(1); }); + + it('contains a modal to start a crawl with selected domains', () => { + const wrapper = shallow(); + + expect(wrapper.find(CrawlSelectDomainsModal)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx index a4b2a9709cd62..63b9c3f080ec2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx @@ -17,6 +17,7 @@ import { EngineLogic, getEngineBreadcrumbs } from '../engine'; import { AppSearchPageTemplate } from '../layout'; import { CrawlRulesTable } from './components/crawl_rules_table'; +import { CrawlSelectDomainsModal } from './components/crawl_select_domains_modal/crawl_select_domains_modal'; import { CrawlerStatusBanner } from './components/crawler_status_banner'; import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator'; import { DeduplicationPanel } from './components/deduplication_panel'; @@ -78,6 +79,7 @@ export const CrawlerSingleDomain: React.FC = () => { + ); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts index c9212bca322d7..fe225f62d1dce 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts @@ -141,6 +141,19 @@ describe('crawler routes', () => { mockRouter.shouldValidate(request); }); + it('validates correctly with overrides', () => { + const request = { + params: { name: 'some-engine' }, + body: { overrides: { domain_allowlist: [] } }, + }; + mockRouter.shouldValidate(request); + }); + + it('validates correctly with empty overrides', () => { + const request = { params: { name: 'some-engine' }, body: { overrides: {} } }; + mockRouter.shouldValidate(request); + }); + it('fails validation without name', () => { const request = { params: {} }; mockRouter.shouldThrow(request); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts index f0fdc5c16098b..5adffe1ff3ee5 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts @@ -63,6 +63,13 @@ export function registerCrawlerRoutes({ params: schema.object({ name: schema.string(), }), + body: schema.object({ + overrides: schema.maybe( + schema.object({ + domain_allowlist: schema.maybe(schema.arrayOf(schema.string())), + }) + ), + }), }, }, enterpriseSearchRequestHandler.createRequest({ From 74f0395c4b99bb06b6d76feac40fde2f79d5aaf9 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 8 Feb 2022 08:28:33 +0200 Subject: [PATCH 15/44] [Lens] Display heatmap on suggestions (#124542) * [Lens] Disaply heatmap on suggestions * Fixes test * disable heatmap suggestions for date histogram buckets * Remove comments * Enable it for multibucket charts * Apply PR comments * Lower the score for date histogram bucket, fix the bug with the heatmap suggestions Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/heatmap_component.tsx | 9 +- .../heatmap_visualization/suggestions.test.ts | 154 +++++++++++++++++- .../heatmap_visualization/suggestions.ts | 31 ++-- 3 files changed, 173 insertions(+), 21 deletions(-) diff --git a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx index 3e6e06de31c62..c1e026064fdfb 100644 --- a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx +++ b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx @@ -544,10 +544,15 @@ export const HeatmapComponent: FC = memo( yAxisLabelName={yAxisColumn?.name} xAxisTitle={args.gridConfig.isXAxisTitleVisible ? xAxisTitle : undefined} yAxisTitle={args.gridConfig.isYAxisTitleVisible ? yAxisTitle : undefined} - xAxisLabelFormatter={(v) => `${xValuesFormatter.convert(v) ?? ''}`} + xAxisLabelFormatter={(v) => + args.gridConfig.isXAxisLabelVisible ? `${xValuesFormatter.convert(v)}` : '' + } yAxisLabelFormatter={ yAxisColumn - ? (v) => `${formatFactory(yAxisColumn.meta.params).convert(v) ?? ''}` + ? (v) => + args.gridConfig.isYAxisLabelVisible + ? `${formatFactory(yAxisColumn.meta.params).convert(v) ?? ''}` + : '' : undefined } /> diff --git a/x-pack/plugins/lens/public/heatmap_visualization/suggestions.test.ts b/x-pack/plugins/lens/public/heatmap_visualization/suggestions.test.ts index 34907c2e93c63..4980adf52e995 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/suggestions.test.ts +++ b/x-pack/plugins/lens/public/heatmap_visualization/suggestions.test.ts @@ -293,14 +293,12 @@ describe('heatmap suggestions', () => { title: 'Heat map', hide: true, previewIcon: 'empty', - score: 0.3, + score: 0, }, ]); }); - }); - describe('shows suggestions', () => { - test('when at least one axis and value accessor are available', () => { + test('when at least one axis has a date histogram', () => { expect( getSuggestions({ table: { @@ -357,21 +355,95 @@ describe('heatmap suggestions', () => { }, }, title: 'Heat map', - // Temp hide all suggestions while heatmap is in beta hide: true, previewIcon: 'empty', + score: 0.3, + }, + ]); + }); + }); + + describe('shows suggestions', () => { + test('when at least one axis and value accessor are available', () => { + expect( + getSuggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [ + { + columnId: 'number-column', + operation: { + isBucketed: true, + dataType: 'number', + scale: 'interval', + label: 'AvgTicketPrice', + }, + }, + { + columnId: 'metric-column', + operation: { + isBucketed: false, + dataType: 'number', + scale: 'ratio', + label: 'Metric', + }, + }, + ], + changeType: 'initial', + }, + state: { + layerId: 'first', + layerType: layerTypes.DATA, + } as HeatmapVisualizationState, + keptLayerIds: ['first'], + }) + ).toEqual([ + { + state: { + layerId: 'first', + layerType: layerTypes.DATA, + shape: 'heatmap', + xAccessor: 'number-column', + valueAccessor: 'metric-column', + gridConfig: { + type: HEATMAP_GRID_FUNCTION, + isCellLabelVisible: false, + isYAxisLabelVisible: true, + isXAxisLabelVisible: true, + isYAxisTitleVisible: false, + isXAxisTitleVisible: false, + }, + legend: { + isVisible: true, + position: Position.Right, + type: LEGEND_FUNCTION, + }, + }, + title: 'Heat map', + hide: false, + previewIcon: 'empty', score: 0.6, }, ]); }); - test('when complete configuration has been resolved', () => { + test('when there is a date histogram and a second bucket dimension', () => { expect( getSuggestions({ table: { layerId: 'first', isMultiRow: true, columns: [ + { + columnId: 'number-column', + operation: { + isBucketed: true, + dataType: 'number', + scale: 'interval', + label: 'AvgTicketPrice', + }, + }, { columnId: 'date-column', operation: { @@ -390,6 +462,71 @@ describe('heatmap suggestions', () => { label: 'Metric', }, }, + ], + changeType: 'initial', + }, + state: { + layerId: 'first', + layerType: layerTypes.DATA, + } as HeatmapVisualizationState, + keptLayerIds: ['first'], + }) + ).toEqual([ + { + state: { + layerId: 'first', + layerType: layerTypes.DATA, + shape: 'heatmap', + yAccessor: 'date-column', + xAccessor: 'number-column', + valueAccessor: 'metric-column', + gridConfig: { + type: HEATMAP_GRID_FUNCTION, + isCellLabelVisible: false, + isYAxisLabelVisible: true, + isXAxisLabelVisible: true, + isYAxisTitleVisible: false, + isXAxisTitleVisible: false, + }, + legend: { + isVisible: true, + position: Position.Right, + type: LEGEND_FUNCTION, + }, + }, + title: 'Heat map', + hide: false, + previewIcon: 'empty', + score: 0.3, + }, + ]); + }); + + test('when complete configuration has been resolved', () => { + expect( + getSuggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [ + { + columnId: 'number-column', + operation: { + isBucketed: true, + dataType: 'number', + scale: 'interval', + label: 'AvgTicketPrice', + }, + }, + { + columnId: 'metric-column', + operation: { + isBucketed: false, + dataType: 'number', + scale: 'ratio', + label: 'Metric', + }, + }, { columnId: 'group-column', operation: { @@ -414,7 +551,7 @@ describe('heatmap suggestions', () => { layerId: 'first', layerType: layerTypes.DATA, shape: 'heatmap', - xAccessor: 'date-column', + xAccessor: 'number-column', yAccessor: 'group-column', valueAccessor: 'metric-column', gridConfig: { @@ -432,8 +569,7 @@ describe('heatmap suggestions', () => { }, }, title: 'Heat map', - // Temp hide all suggestions while heatmap is in beta - hide: true, + hide: false, previewIcon: 'empty', score: 0.9, }, diff --git a/x-pack/plugins/lens/public/heatmap_visualization/suggestions.ts b/x-pack/plugins/lens/public/heatmap_visualization/suggestions.ts index fac07d322e037..52c7a1bfd6d26 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/suggestions.ts +++ b/x-pack/plugins/lens/public/heatmap_visualization/suggestions.ts @@ -58,9 +58,17 @@ export const getSuggestions: Visualization['getSugges * Hide for: * - reduced and reorder tables * - tables with just a single bucket dimension + * - tables with only date histogram */ + const hasOnlyDatehistogramBuckets = + metrics.length === 1 && + groups.length > 0 && + groups.every((group) => group.operation.dataType === 'date'); const hide = - table.changeType === 'reduced' || table.changeType === 'reorder' || isSingleBucketDimension; + table.changeType === 'reduced' || + table.changeType === 'reorder' || + isSingleBucketDimension || + hasOnlyDatehistogramBuckets; const newState: HeatmapVisualizationState = { shape: CHART_SHAPES.HEATMAP, @@ -74,8 +82,8 @@ export const getSuggestions: Visualization['getSugges gridConfig: { type: HEATMAP_GRID_FUNCTION, isCellLabelVisible: false, - isYAxisLabelVisible: true, - isXAxisLabelVisible: true, + isYAxisLabelVisible: state?.gridConfig?.isYAxisLabelVisible ?? true, + isXAxisLabelVisible: state?.gridConfig?.isXAxisLabelVisible ?? true, isYAxisTitleVisible: state?.gridConfig?.isYAxisTitleVisible ?? false, isXAxisTitleVisible: state?.gridConfig?.isXAxisTitleVisible ?? false, }, @@ -93,11 +101,15 @@ export const getSuggestions: Visualization['getSugges newState.xAccessor = histogram[0]?.columnId || ordinal[0]?.columnId; newState.yAccessor = groups.find((g) => g.columnId !== newState.xAccessor)?.columnId; - if (newState.xAccessor) { - score += 0.3; - } - if (newState.yAccessor) { - score += 0.3; + const hasDatehistogram = groups.some((group) => group.operation.dataType === 'date'); + + if (!hasDatehistogram) { + if (newState.xAccessor) { + score += 0.3; + } + if (newState.yAccessor) { + score += 0.3; + } } return [ @@ -106,8 +118,7 @@ export const getSuggestions: Visualization['getSugges title: i18n.translate('xpack.lens.heatmap.heatmapLabel', { defaultMessage: 'Heat map', }), - // Temp hide all suggestions while heatmap is in beta - hide: true || hide, + hide, previewIcon: 'empty', score: Number(score.toFixed(1)), }, From a248af003df81341ac66da2647a2755bac0eef6d Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 8 Feb 2022 10:22:21 +0100 Subject: [PATCH 16/44] [ML] Functional tests for the Overview page and side nav (#124793) * update side nav tests for full ml access * ML nodes tests * assert empty states * assert getting started callout * assert panels with data * assert read ml access * waitForDatePickerIndicatorLoaded * add missing step * fix typo * rename variable --- .../components/ml_page/side_nav.tsx | 5 +- .../application/components/stats_bar/stat.tsx | 4 +- .../components/getting_started_callout.tsx | 7 ++- .../nodes_overview/nodes_list.tsx | 5 +- .../apps/ml/permissions/full_ml_access.ts | 61 ++++++++++++++++--- .../apps/ml/permissions/read_ml_access.ts | 33 ++++++++-- .../test/functional/services/ml/common_ui.ts | 4 ++ x-pack/test/functional/services/ml/index.ts | 3 + .../functional/services/ml/ml_nodes_list.ts | 41 +++++++++++++ .../test/functional/services/ml/navigation.ts | 24 ++++++++ .../functional/services/ml/overview_page.ts | 30 +++++++++ 11 files changed, 196 insertions(+), 21 deletions(-) create mode 100644 x-pack/test/functional/services/ml/ml_nodes_list.ts diff --git a/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx b/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx index 872337f71fe82..39672b14bf836 100644 --- a/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx @@ -111,6 +111,7 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) { }), pathId: ML_PAGES.SINGLE_METRIC_VIEWER, disabled: disableLinks, + testSubj: 'mlMainTab singleMetricViewer', }, { id: 'settings', @@ -185,7 +186,7 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) { defaultMessage: 'File', }), disabled: false, - testSubj: 'mlMainTab dataVisualizer fileDatavisualizer', + testSubj: 'mlMainTab fileDataVisualizer', }, { id: 'data_view_datavisualizer', @@ -194,7 +195,7 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) { defaultMessage: 'Data View', }), disabled: false, - testSubj: 'mlMainTab dataVisualizer dataViewDatavisualizer', + testSubj: 'mlMainTab indexDataVisualizer', }, ], }, diff --git a/x-pack/plugins/ml/public/application/components/stats_bar/stat.tsx b/x-pack/plugins/ml/public/application/components/stats_bar/stat.tsx index e08e75143447c..ae04a7a3b2448 100644 --- a/x-pack/plugins/ml/public/application/components/stats_bar/stat.tsx +++ b/x-pack/plugins/ml/public/application/components/stats_bar/stat.tsx @@ -11,6 +11,7 @@ export interface StatsBarStat { label: string; value: number; show?: boolean; + 'data-test-subj'?: string; } interface StatProps { stat: StatsBarStat; @@ -19,7 +20,8 @@ interface StatProps { export const Stat: FC = ({ stat }) => { return ( - {stat.label}: {stat.value} + {stat.label}:{' '} + {stat.value} ); }; diff --git a/x-pack/plugins/ml/public/application/overview/components/getting_started_callout.tsx b/x-pack/plugins/ml/public/application/overview/components/getting_started_callout.tsx index f9d734f7b84d5..457bca80ee2c0 100644 --- a/x-pack/plugins/ml/public/application/overview/components/getting_started_callout.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/getting_started_callout.tsx @@ -31,6 +31,7 @@ export const GettingStartedCallout: FC = () => { return ( <> { />

- + = ({ compactView = false }) => { label: i18n.translate('xpack.ml.trainedModels.nodesList.totalAmountLabel', { defaultMessage: 'Total machine learning nodes', }), + 'data-test-subj': 'mlTotalNodesCount', }, }; }, [items]); @@ -189,7 +190,7 @@ export const NodesList: FC = ({ compactView = false }) => { } return ( - <> +

{nodesStats && ( @@ -218,6 +219,6 @@ export const NodesList: FC = ({ compactView = false }) => { data-test-subj={isLoading ? 'mlNodesTable loading' : 'mlNodesTable loaded'} />
- + ); }; diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts index 71c0d101943b9..c038aeba608bd 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -14,6 +14,7 @@ import { USER } from '../../../services/ml/security_common'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); + const browser = getService('browser'); const testUsers = [ { user: USER.ML_POWERUSER, discoverAvailable: true }, @@ -44,7 +45,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.assertKibanaNavMLEntryExists(); }); - it('should display tabs in the ML app correctly', async () => { + it('should display side nav in the ML app correctly', async () => { await ml.testExecution.logTestStep('should load the ML app'); await ml.navigation.navigateToMl(); @@ -52,33 +53,60 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.assertOverviewTabEnabled(true); await ml.testExecution.logTestStep( - 'should display the enabled "Anomaly Detection" tab' + 'should display the enabled "Anomaly Detection" section correctly' ); await ml.navigation.assertAnomalyDetectionTabEnabled(true); + await ml.navigation.assertAnomalyExplorerNavItemEnabled(true); + await ml.navigation.assertSingleMetricViewerNavItemEnabled(true); + await ml.navigation.assertSettingsTabEnabled(true); await ml.testExecution.logTestStep( - 'should display the enabled "Data Frame Analytics" tab' + 'should display the enabled "Data Frame Analytics" section' ); await ml.navigation.assertDataFrameAnalyticsTabEnabled(true); - await ml.testExecution.logTestStep('should display the enabled "Data Visualizer" tab'); - await ml.navigation.assertDataVisualizerTabEnabled(true); + await ml.testExecution.logTestStep( + 'should display the enabled "Model Management" section' + ); + await ml.navigation.assertTrainedModelsNavItemEnabled(true); + await ml.navigation.assertNodesNavItemEnabled(true); - await ml.testExecution.logTestStep('should display the enabled "Settings" tab'); - await ml.navigation.assertSettingsTabEnabled(true); + await ml.testExecution.logTestStep( + 'should display the enabled "Data Visualizer" section' + ); + await ml.navigation.assertDataVisualizerTabEnabled(true); + await ml.navigation.assertFileDataVisualizerNavItemEnabled(true); + await ml.navigation.assertIndexDataVisualizerNavItemEnabled(true); }); it('should display elements on ML Overview page correctly', async () => { await ml.testExecution.logTestStep('should load the ML overview page'); await ml.navigation.navigateToOverview(); - await ml.testExecution.logTestStep('should display enabled AD create job button'); + await ml.commonUI.waitForDatePickerIndicatorLoaded(); + + await ml.testExecution.logTestStep('should display a welcome callout'); + await ml.overviewPage.assertGettingStartedCalloutVisible(true); + await ml.overviewPage.dismissGettingStartedCallout(); + + await ml.testExecution.logTestStep('should display ML Nodes panel'); + await ml.mlNodesPanel.assertNodeOverviewPanel(); + + await ml.testExecution.logTestStep('should display Anomaly Detection empty state'); + await ml.overviewPage.assertADEmptyStateExists(); await ml.overviewPage.assertADCreateJobButtonExists(); await ml.overviewPage.assertADCreateJobButtonEnabled(true); - await ml.testExecution.logTestStep('should display enabled DFA create job button'); + await ml.testExecution.logTestStep('should display DFA empty state'); + await ml.overviewPage.assertDFAEmptyStateExists(); await ml.overviewPage.assertDFACreateJobButtonExists(); await ml.overviewPage.assertDFACreateJobButtonEnabled(true); + + await ml.testExecution.logTestStep( + 'should persist the getting started callout state after refresh' + ); + await browser.refresh(); + await ml.overviewPage.assertGettingStartedCalloutVisible(false); }); }); } @@ -164,6 +192,21 @@ export default function ({ getService }: FtrProviderContext) { await ml.securityUI.logout(); }); + it('should display elements on ML Overview page correctly', async () => { + await ml.testExecution.logTestStep('should load the Overview page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToOverview(); + + await ml.testExecution.logTestStep('should display ML Nodes panel'); + await ml.mlNodesPanel.assertNodeOverviewPanel(); + + await ml.testExecution.logTestStep('should display Anomaly Detection panel'); + await ml.overviewPage.assertAdJobsOverviewPanelExist(); + + await ml.testExecution.logTestStep('should display DFA panel'); + await ml.overviewPage.assertDFAJobsOverviewPanelExist(); + }); + it('should display elements on Anomaly Detection page correctly', async () => { await ml.testExecution.logTestStep('should load the AD job management page'); await ml.navigation.navigateToMl(); diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts index 9abb30548b0eb..fd9cb2cb4c79e 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -52,20 +52,30 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.assertOverviewTabEnabled(true); await ml.testExecution.logTestStep( - 'should display the enabled "Anomaly Detection" tab' + 'should display the enabled "Anomaly Detection" section correctly' ); await ml.navigation.assertAnomalyDetectionTabEnabled(true); + await ml.navigation.assertAnomalyExplorerNavItemEnabled(true); + await ml.navigation.assertSingleMetricViewerNavItemEnabled(true); + await ml.navigation.assertSettingsTabEnabled(true); await ml.testExecution.logTestStep( - 'should display the enabled "Data Frame Analytics" tab' + 'should display the enabled "Data Frame Analytics" section' ); await ml.navigation.assertDataFrameAnalyticsTabEnabled(true); - await ml.testExecution.logTestStep('should display the enabled "Data Visualizer" tab'); - await ml.navigation.assertDataVisualizerTabEnabled(true); + await ml.testExecution.logTestStep( + 'should display the enabled "Model Management" section' + ); + await ml.navigation.assertTrainedModelsNavItemEnabled(true); + await ml.navigation.assertNodesNavItemEnabled(false); - await ml.testExecution.logTestStep('should display the enabled "Settings" tab'); - await ml.navigation.assertSettingsTabEnabled(true); + await ml.testExecution.logTestStep( + 'should display the enabled "Data Visualizer" section' + ); + await ml.navigation.assertDataVisualizerTabEnabled(true); + await ml.navigation.assertFileDataVisualizerNavItemEnabled(true); + await ml.navigation.assertIndexDataVisualizerNavItemEnabled(true); }); it('should display elements on ML Overview page correctly', async () => { @@ -73,11 +83,22 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToMl(); await ml.navigation.navigateToOverview(); + await ml.commonUI.waitForDatePickerIndicatorLoaded(); + + await ml.testExecution.logTestStep('should display a welcome callout'); + await ml.overviewPage.assertGettingStartedCalloutVisible(true); + await ml.overviewPage.dismissGettingStartedCallout(); + + await ml.testExecution.logTestStep('should not display ML Nodes panel'); + await ml.mlNodesPanel.assertNodesOverviewPanelExists(false); + await ml.testExecution.logTestStep('should display disabled AD create job button'); + await ml.overviewPage.assertADEmptyStateExists(); await ml.overviewPage.assertADCreateJobButtonExists(); await ml.overviewPage.assertADCreateJobButtonEnabled(false); await ml.testExecution.logTestStep('should display disabled DFA create job button'); + await ml.overviewPage.assertDFAEmptyStateExists(); await ml.overviewPage.assertDFACreateJobButtonExists(); await ml.overviewPage.assertDFACreateJobButtonEnabled(false); }); diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index cb9ef179f0626..d6b75f53578a8 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -334,5 +334,9 @@ export function MachineLearningCommonUIProvider({ await PageObjects.spaceSelector.goToSpecificSpace(spaceId); await PageObjects.spaceSelector.expectHomePage(spaceId); }, + + async waitForDatePickerIndicatorLoaded() { + await testSubjects.waitForEnabled('superDatePickerApplyTimeButton'); + }, }; } diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index 4b48e4c0269eb..f7fd5efefda33 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -54,6 +54,7 @@ import { MachineLearningDashboardEmbeddablesProvider } from './dashboard_embedda import { TrainedModelsProvider } from './trained_models'; import { TrainedModelsTableProvider } from './trained_models_table'; import { MachineLearningJobAnnotationsProvider } from './job_annotations_table'; +import { MlNodesPanelProvider } from './ml_nodes_list'; export function MachineLearningProvider(context: FtrProviderContext) { const commonAPI = MachineLearningCommonAPIProvider(context); @@ -124,6 +125,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const swimLane = SwimLaneProvider(context); const trainedModels = TrainedModelsProvider(context, api, commonUI); const trainedModelsTable = TrainedModelsTableProvider(context); + const mlNodesPanel = MlNodesPanelProvider(context); return { anomaliesTable, @@ -173,5 +175,6 @@ export function MachineLearningProvider(context: FtrProviderContext) { testResources, trainedModels, trainedModelsTable, + mlNodesPanel, }; } diff --git a/x-pack/test/functional/services/ml/ml_nodes_list.ts b/x-pack/test/functional/services/ml/ml_nodes_list.ts new file mode 100644 index 0000000000000..37cd4143e26cc --- /dev/null +++ b/x-pack/test/functional/services/ml/ml_nodes_list.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function MlNodesPanelProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async assertNodesOverviewPanelExists(expectPanelExits: boolean = true) { + if (expectPanelExits) { + await testSubjects.existOrFail('mlNodesOverviewPanel'); + } else { + await testSubjects.missingOrFail('mlNodesOverviewPanel'); + } + }, + + async assertNodesListLoaded() { + await testSubjects.existOrFail('mlNodesTable loaded', { timeout: 5000 }); + }, + + async assertMlNodesCount(minCount: number = 1) { + const actualCount = parseInt(await testSubjects.getVisibleText('mlTotalNodesCount'), 10); + expect(actualCount).to.not.be.lessThan( + minCount, + `Total ML nodes count should be at least '${minCount}' (got '${actualCount}')` + ); + }, + + async assertNodeOverviewPanel() { + await this.assertNodesOverviewPanelExists(); + await this.assertNodesListLoaded(); + await this.assertMlNodesCount(); + }, + }; +} diff --git a/x-pack/test/functional/services/ml/navigation.ts b/x-pack/test/functional/services/ml/navigation.ts index c11721453d10f..6bf753926c72a 100644 --- a/x-pack/test/functional/services/ml/navigation.ts +++ b/x-pack/test/functional/services/ml/navigation.ts @@ -106,14 +106,38 @@ export function MachineLearningNavigationProvider({ await this.assertTabEnabled('~mlMainTab & ~anomalyDetection', expectedValue); }, + async assertAnomalyExplorerNavItemEnabled(expectedValue: boolean) { + await this.assertTabEnabled('~mlMainTab & ~anomalyExplorer', expectedValue); + }, + + async assertSingleMetricViewerNavItemEnabled(expectedValue: boolean) { + await this.assertTabEnabled('~mlMainTab & ~singleMetricViewer', expectedValue); + }, + async assertDataFrameAnalyticsTabEnabled(expectedValue: boolean) { await this.assertTabEnabled('~mlMainTab & ~dataFrameAnalytics', expectedValue); }, + async assertTrainedModelsNavItemEnabled(expectedValue: boolean) { + await this.assertTabEnabled('~mlMainTab & ~trainedModels', expectedValue); + }, + + async assertNodesNavItemEnabled(expectedValue: boolean) { + await this.assertTabEnabled('~mlMainTab & ~nodesOverview', expectedValue); + }, + async assertDataVisualizerTabEnabled(expectedValue: boolean) { await this.assertTabEnabled('~mlMainTab & ~dataVisualizer', expectedValue); }, + async assertFileDataVisualizerNavItemEnabled(expectedValue: boolean) { + await this.assertTabEnabled('~mlMainTab & ~fileDataVisualizer', expectedValue); + }, + + async assertIndexDataVisualizerNavItemEnabled(expectedValue: boolean) { + await this.assertTabEnabled('~mlMainTab & ~indexDataVisualizer', expectedValue); + }, + async assertSettingsTabEnabled(expectedValue: boolean) { await this.assertTabEnabled('~mlMainTab & ~settings', expectedValue); }, diff --git a/x-pack/test/functional/services/ml/overview_page.ts b/x-pack/test/functional/services/ml/overview_page.ts index 8fc04dfa29b18..5f02edde0f310 100644 --- a/x-pack/test/functional/services/ml/overview_page.ts +++ b/x-pack/test/functional/services/ml/overview_page.ts @@ -13,6 +13,24 @@ export function MachineLearningOverviewPageProvider({ getService }: FtrProviderC const testSubjects = getService('testSubjects'); return { + async assertGettingStartedCalloutVisible(expectVisible: boolean = true) { + if (expectVisible) { + await testSubjects.existOrFail('mlGettingStartedCallout'); + } else { + await testSubjects.missingOrFail('mlGettingStartedCallout'); + } + }, + + async dismissGettingStartedCallout() { + await this.assertGettingStartedCalloutVisible(true); + await testSubjects.click('mlDismissGettingStartedCallout'); + await this.assertGettingStartedCalloutVisible(false); + }, + + async assertADEmptyStateExists() { + await testSubjects.existOrFail('mlAnomalyDetectionEmptyState'); + }, + async assertADCreateJobButtonExists() { await testSubjects.existOrFail('mlCreateNewJobButton'); }, @@ -27,6 +45,14 @@ export function MachineLearningOverviewPageProvider({ getService }: FtrProviderC ); }, + async assertAdJobsOverviewPanelExist() { + await testSubjects.existOrFail('mlOverviewTableAnomalyDetection'); + }, + + async assertDFAEmptyStateExists() { + await testSubjects.existOrFail('mlNoDataFrameAnalyticsFound'); + }, + async assertDFACreateJobButtonExists() { await testSubjects.existOrFail('mlAnalyticsCreateFirstButton'); }, @@ -41,6 +67,10 @@ export function MachineLearningOverviewPageProvider({ getService }: FtrProviderC ); }, + async assertDFAJobsOverviewPanelExist() { + await testSubjects.existOrFail('mlOverviewTableAnalytics'); + }, + async assertJobSyncRequiredWarningExists() { await testSubjects.existOrFail('mlJobSyncRequiredWarning', { timeout: 5000 }); }, From c299aabcb32af15d34734258b7b76a5b0d990ecc Mon Sep 17 00:00:00 2001 From: Diana Derevyankina <54894989+DziyanaDzeraviankina@users.noreply.github.com> Date: Tue, 8 Feb 2022 13:02:15 +0300 Subject: [PATCH 17/44] TSVB needs to display better UX message when default index pattern is non-time based (#124341) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/vis_types/timeseries/common/errors.ts | 10 ++++++++++ .../server/lib/vis_data/get_interval_and_timefield.ts | 7 ++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/plugins/vis_types/timeseries/common/errors.ts b/src/plugins/vis_types/timeseries/common/errors.ts index 8acb8f201a780..d3836fd0cd2a7 100644 --- a/src/plugins/vis_types/timeseries/common/errors.ts +++ b/src/plugins/vis_types/timeseries/common/errors.ts @@ -58,6 +58,16 @@ export class AggNotSupportedError extends UIError { } } +export class TimeFieldNotSpecifiedError extends UIError { + constructor() { + super( + i18n.translate('visTypeTimeseries.errors.timeFieldNotSpecifiedError', { + defaultMessage: 'Time field is required to visualize the data', + }) + ); + } +} + export const filterCannotBeAppliedErrorMessage = i18n.translate( 'visTypeTimeseries.filterCannotBeAppliedError', { diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts index 7c17f003dfbab..af6eb44affabc 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/get_interval_and_timefield.ts @@ -9,6 +9,7 @@ import moment from 'moment'; import { AUTO_INTERVAL } from '../../../common/constants'; import { validateField } from '../../../common/fields_utils'; import { validateInterval } from '../../../common/validate_interval'; +import { TimeFieldNotSpecifiedError } from '../../../common/errors'; import type { FetchedIndexPattern, Panel, Series } from '../../../common/types'; @@ -34,7 +35,11 @@ export function getIntervalAndTimefield( } if (panel.use_kibana_indexes) { - validateField(timeField!, index); + if (timeField) { + validateField(timeField, index); + } else { + throw new TimeFieldNotSpecifiedError(); + } } let interval = panel.interval; From e312c36e4cf67dad48a58fdcd947eab022934315 Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Tue, 8 Feb 2022 06:13:06 -0500 Subject: [PATCH 18/44] [Security Solution] Remove a data fetching hook from the add to timeline action component (#124331) * Fetch alert ecs data in actions.tsx and not a hook in every table row * Add error handling and tests for theshold timelines * Fix bad merge * Remove unused imports * Actually remove unused file * Remove usage of alertIds and dead code from cases * Add basic sanity tests that ensure no extra network calls are being made * Remove unused operator * Remove unused imports * Remove unused mock --- .../public/components/__mock__/timeline.tsx | 2 - .../components/case_view/case_view_page.tsx | 3 - .../components/timeline_context/index.tsx | 1 - .../public/components/user_actions/types.ts | 1 - .../public/cases/pages/index.tsx | 14 -- .../public/common/lib/kibana/services.ts | 6 +- .../components/alerts_table/actions.test.tsx | 194 ++++++++++++++---- .../components/alerts_table/actions.tsx | 135 ++++++++---- .../investigate_in_timeline_action.test.tsx | 81 ++++++++ .../investigate_in_timeline_action.tsx | 3 - .../use_investigate_in_timeline.test.tsx | 82 ++++++++ .../use_investigate_in_timeline.tsx | 59 +++--- .../components/take_action_dropdown/index.tsx | 6 - .../alerts/use_fetch_ecs_alerts_data.ts | 83 -------- .../side_panel/event_details/footer.test.tsx | 131 ++++++++++++ .../side_panel/event_details/footer.tsx | 22 +- .../timeline/body/actions/index.tsx | 3 - .../t_grid/body/control_columns/checkbox.tsx | 6 +- 18 files changed, 579 insertions(+), 253 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.test.tsx diff --git a/x-pack/plugins/cases/public/components/__mock__/timeline.tsx b/x-pack/plugins/cases/public/components/__mock__/timeline.tsx index 0aeda0f08302d..d576b0ef1732c 100644 --- a/x-pack/plugins/cases/public/components/__mock__/timeline.tsx +++ b/x-pack/plugins/cases/public/components/__mock__/timeline.tsx @@ -24,8 +24,6 @@ export const timelineIntegrationMock = { useInsertTimeline: jest.fn(), }, ui: { - renderInvestigateInTimelineActionComponent: () => - mockTimelineComponent('investigate-in-timeline'), renderTimelineDetailsPanel: () => mockTimelineComponent('timeline-details-panel'), }, }; diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx index c0dc4fa4d95e9..235c8eabc9e59 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx @@ -356,9 +356,6 @@ export const CaseViewPage = React.memo( isLoadingUserActions={isLoadingUserActions} onShowAlertDetails={onShowAlertDetails} onUpdateField={onUpdateField} - renderInvestigateInTimelineActionComponent={ - timelineUi?.renderInvestigateInTimelineActionComponent - } statusActionButton={ userCanCrud ? ( UseInsertTimelineReturn; }; ui?: { - renderInvestigateInTimelineActionComponent?: (alertIds: string[]) => JSX.Element; renderTimelineDetailsPanel?: () => JSX.Element; }; } diff --git a/x-pack/plugins/cases/public/components/user_actions/types.ts b/x-pack/plugins/cases/public/components/user_actions/types.ts index 80657cc90cba9..dece59ec1eb42 100644 --- a/x-pack/plugins/cases/public/components/user_actions/types.ts +++ b/x-pack/plugins/cases/public/components/user_actions/types.ts @@ -28,7 +28,6 @@ export interface UserActionTreeProps { onRuleDetailsClick?: RuleDetailsNavigation['onClick']; onShowAlertDetails: (alertId: string, index: string) => void; onUpdateField: ({ key, value, onSuccess, onError }: OnUpdateFields) => void; - renderInvestigateInTimelineActionComponent?: (alertIds: string[]) => JSX.Element; statusActionButton: JSX.Element | null; updateCase: (newCase: Case) => void; useFetchAlertData: (alertIds: string[]) => [boolean, Record]; diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.tsx index 08878b5615257..1f02ff88b19bd 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/index.tsx @@ -12,7 +12,6 @@ import { TimelineId } from '../../../common/types/timeline'; import { getRuleDetailsUrl, useFormatUrl } from '../../common/components/link_to'; -import * as i18n from './translations'; import { useGetUserCasesPermissions, useKibana, useNavigation } from '../../common/lib/kibana'; import { APP_ID, CASES_PATH, SecurityPageName } from '../../../common/constants'; import { timelineActions } from '../../timelines/store/timeline'; @@ -25,7 +24,6 @@ import { SpyRoute } from '../../common/utils/route/spy_routes'; import { useInsertTimeline } from '../components/use_insert_timeline'; import * as timelineMarkdownPlugin from '../../common/components/markdown_editor/plugins/timeline'; import { DetailsPanel } from '../../timelines/components/side_panel'; -import { InvestigateInTimelineAction } from '../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action'; import { useFetchAlertData } from './use_fetch_alert_data'; const TimelineDetailsPanel = () => { @@ -44,17 +42,6 @@ const TimelineDetailsPanel = () => { ); }; -const InvestigateInTimelineActionComponent = (alertIds: string[]) => { - return ( - - ); -}; - const CaseContainerComponent: React.FC = () => { const { cases: casesUi } = useKibana().services; const { getAppUrl, navigateTo } = useNavigation(); @@ -163,7 +150,6 @@ const CaseContainerComponent: React.FC = () => { useInsertTimeline, }, ui: { - renderInvestigateInTimelineActionComponent: InvestigateInTimelineActionComponent, renderTimelineDetailsPanel: TimelineDetailsPanel, }, }, diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/services.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/services.ts index 364eea3f8d98b..30bd3d6e59896 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/services.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/services.ts @@ -8,7 +8,8 @@ import { CoreStart } from '../../../../../../../src/core/public'; import { StartPlugins } from '../../../types'; -type GlobalServices = Pick & Pick; +type GlobalServices = Pick & + Pick; export class KibanaServices { private static kibanaVersion?: string; @@ -19,8 +20,9 @@ export class KibanaServices { data, kibanaVersion, uiSettings, + notifications, }: GlobalServices & { kibanaVersion: string }) { - this.services = { data, http, uiSettings }; + this.services = { data, http, uiSettings, notifications }; this.kibanaVersion = kibanaVersion; } diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index ad95f89c850f6..b1226e5b59190 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -29,11 +29,18 @@ import type { ISearchStart } from '../../../../../../../src/plugins/data/public' import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { getTimelineTemplate } from '../../../timelines/containers/api'; import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers'; +import { KibanaServices } from '../../../common/lib/kibana'; +import { + DEFAULT_FROM_MOMENT, + DEFAULT_TO_MOMENT, +} from '../../../common/utils/default_date_settings'; jest.mock('../../../timelines/containers/api', () => ({ getTimelineTemplate: jest.fn(), })); +jest.mock('../../../common/lib/kibana'); + describe('alert actions', () => { const anchor = '2020-03-01T17:59:46.349Z'; const unix = moment(anchor).valueOf(); @@ -41,6 +48,9 @@ describe('alert actions', () => { let updateTimelineIsLoading: UpdateTimelineLoading; let searchStrategyClient: jest.Mocked; let clock: sinon.SinonFakeTimers; + let mockKibanaServices: jest.Mock; + let fetchMock: jest.Mock; + let toastMock: jest.Mock; beforeEach(() => { // jest carries state between mocked implementations when using @@ -52,6 +62,14 @@ describe('alert actions', () => { createTimeline = jest.fn() as jest.Mocked; updateTimelineIsLoading = jest.fn() as jest.Mocked; + mockKibanaServices = KibanaServices.get as jest.Mock; + + fetchMock = jest.fn(); + toastMock = jest.fn(); + mockKibanaServices.mockReturnValue({ + http: { fetch: fetchMock }, + notifications: { toasts: { addError: toastMock } }, + }); searchStrategyClient = { ...dataPluginMock.createStartContract().search, @@ -418,6 +436,59 @@ describe('alert actions', () => { }); describe('determineToAndFrom', () => { + const ecsDataMockWithNoTemplateTimeline = getThresholdDetectionAlertAADMock({ + ...mockAADEcsDataWithAlert, + kibana: { + alert: { + ...mockAADEcsDataWithAlert.kibana?.alert, + rule: { + ...mockAADEcsDataWithAlert.kibana?.alert?.rule, + parameters: { + ...mockAADEcsDataWithAlert.kibana?.alert?.rule?.parameters, + threshold: { + field: ['destination.ip'], + value: 1, + }, + }, + name: ['mock threshold rule'], + saved_id: [], + type: ['threshold'], + uuid: ['c5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'], + timeline_id: undefined, + timeline_title: undefined, + }, + threshold_result: { + count: 99, + from: '2021-01-10T21:11:45.839Z', + cardinality: [ + { + field: 'source.ip', + value: 1, + }, + ], + terms: [ + { + field: 'destination.ip', + value: 1, + }, + ], + }, + }, + }, + }); + beforeEach(() => { + fetchMock.mockResolvedValue({ + hits: { + hits: [ + { + _id: ecsDataMockWithNoTemplateTimeline[0]._id, + _index: 'mock', + _source: ecsDataMockWithNoTemplateTimeline[0], + }, + ], + }, + }); + }); test('it uses ecs.Data.timestamp if one is provided', () => { const ecsDataMock: Ecs = { ...mockEcsDataWithAlert, @@ -438,47 +509,6 @@ describe('alert actions', () => { }); test('it uses original_time and threshold_result.from for threshold alerts', async () => { - const ecsDataMockWithNoTemplateTimeline = getThresholdDetectionAlertAADMock({ - ...mockAADEcsDataWithAlert, - kibana: { - alert: { - ...mockAADEcsDataWithAlert.kibana?.alert, - rule: { - ...mockAADEcsDataWithAlert.kibana?.alert?.rule, - parameters: { - ...mockAADEcsDataWithAlert.kibana?.alert?.rule?.parameters, - threshold: { - field: ['destination.ip'], - value: 1, - }, - }, - name: ['mock threshold rule'], - saved_id: [], - type: ['threshold'], - uuid: ['c5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'], - timeline_id: undefined, - timeline_title: undefined, - }, - threshold_result: { - count: 99, - from: '2021-01-10T21:11:45.839Z', - cardinality: [ - { - field: 'source.ip', - value: 1, - }, - ], - terms: [ - { - field: 'destination.ip', - value: 1, - }, - ], - }, - }, - }, - }); - const expectedFrom = '2021-01-10T21:11:45.839Z'; const expectedTo = '2021-01-10T21:12:45.839Z'; @@ -525,4 +555,86 @@ describe('alert actions', () => { }); }); }); + + describe('show toasts when data is malformed', () => { + const ecsDataMockWithNoTemplateTimeline = getThresholdDetectionAlertAADMock({ + ...mockAADEcsDataWithAlert, + kibana: { + alert: { + ...mockAADEcsDataWithAlert.kibana?.alert, + rule: { + ...mockAADEcsDataWithAlert.kibana?.alert?.rule, + parameters: { + ...mockAADEcsDataWithAlert.kibana?.alert?.rule?.parameters, + threshold: { + field: ['destination.ip'], + value: 1, + }, + }, + name: ['mock threshold rule'], + saved_id: [], + type: ['threshold'], + uuid: ['c5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'], + timeline_id: undefined, + timeline_title: undefined, + }, + threshold_result: { + count: 99, + from: '2021-01-10T21:11:45.839Z', + cardinality: [ + { + field: 'source.ip', + value: 1, + }, + ], + terms: [ + { + field: 'destination.ip', + value: 1, + }, + ], + }, + }, + }, + }); + beforeEach(() => { + fetchMock.mockResolvedValue({ + hits: 'not correctly formed doc', + }); + }); + test('renders a toast and calls create timeline with basic defaults', async () => { + const expectedFrom = DEFAULT_FROM_MOMENT.toISOString(); + const expectedTo = DEFAULT_TO_MOMENT.toISOString(); + const timelineProps = { + ...defaultTimelineProps, + timeline: { + ...defaultTimelineProps.timeline, + dataProviders: [], + dateRange: { + start: expectedFrom, + end: expectedTo, + }, + description: '', + kqlQuery: { + filterQuery: null, + }, + resolveTimelineConfig: undefined, + }, + from: expectedFrom, + to: expectedTo, + }; + + delete timelineProps.ruleNote; + + await sendAlertToTimelineAction({ + createTimeline, + ecsData: ecsDataMockWithNoTemplateTimeline, + updateTimelineIsLoading, + searchStrategyClient, + }); + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimeline).toHaveBeenCalledWith(timelineProps); + expect(toastMock).toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index c12133089e02a..46e439d38f81e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -21,6 +21,7 @@ import { ALERT_RULE_PARAMETERS, } from '@kbn/rule-data-utils'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ALERT_ORIGINAL_TIME, ALERT_GROUP_ID, @@ -64,6 +65,13 @@ import { QueryOperator, } from '../../../timelines/components/timeline/data_providers/data_provider'; import { getTimelineTemplate } from '../../../timelines/containers/api'; +import { KibanaServices } from '../../../common/lib/kibana'; +import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../common/constants'; +import { buildAlertsQuery, formatAlertToEcsSignal } from '../../../common/utils/alerts'; +import { + DEFAULT_FROM_MOMENT, + DEFAULT_TO_MOMENT, +} from '../../../common/utils/default_date_settings'; export const getUpdateAlertsQuery = (eventIds: Readonly) => { return { @@ -177,7 +185,7 @@ export const getThresholdAggregationData = (ecsData: Ecs | Ecs[]): ThresholdAggr return thresholdEcsData.reduce( (outerAcc, thresholdData) => { const threshold = - getField(thresholdData, ALERT_RULE_PARAMETERS).threshold ?? + getField(thresholdData, `${ALERT_RULE_PARAMETERS}.threshold`) ?? thresholdData.signal?.rule?.threshold; const thresholdResult: { @@ -384,47 +392,102 @@ const buildEqlDataProviderOrFilter = ( return { filters: [], dataProviders: [] }; }; -const createThresholdTimeline = ( +const createThresholdTimeline = async ( ecsData: Ecs, createTimeline: ({ from, timeline, to }: CreateTimelineProps) => void, noteContent: string, templateValues: { filters?: Filter[]; query?: string; dataProviders?: DataProvider[] } ) => { - const { thresholdFrom, thresholdTo, dataProviders } = getThresholdAggregationData(ecsData); - const params = getField(ecsData, ALERT_RULE_PARAMETERS); - const filters = getFiltersFromRule(params.filters ?? ecsData.signal?.rule?.filters) ?? []; - const language = params.language ?? ecsData.signal?.rule?.language ?? 'kuery'; - const query = params.query ?? ecsData.signal?.rule?.query ?? ''; - const indexNames = params.index ?? ecsData.signal?.rule?.index ?? []; - - return createTimeline({ - from: thresholdFrom, - notes: null, - timeline: { - ...timelineDefaults, - description: `_id: ${ecsData._id}`, - filters: templateValues.filters ?? filters, - dataProviders: templateValues.dataProviders ?? dataProviders, - id: TimelineId.active, - indexNames, - dateRange: { - start: thresholdFrom, - end: thresholdTo, - }, - eventType: 'all', - kqlQuery: { - filterQuery: { - kuery: { - kind: language, - expression: templateValues.query ?? query, + try { + const alertResponse = await KibanaServices.get().http.fetch< + estypes.SearchResponse<{ '@timestamp': string; [key: string]: unknown }> + >(DETECTION_ENGINE_QUERY_SIGNALS_URL, { + method: 'POST', + body: JSON.stringify(buildAlertsQuery([ecsData._id])), + }); + const formattedAlertData = + alertResponse?.hits.hits.reduce((acc, { _id, _index, _source = {} }) => { + return [ + ...acc, + { + ...formatAlertToEcsSignal(_source), + _id, + _index, + timestamp: _source['@timestamp'], + }, + ]; + }, []) ?? []; + const alertDoc = formattedAlertData[0]; + const params = getField(alertDoc, ALERT_RULE_PARAMETERS); + const filters = getFiltersFromRule(params.filters ?? alertDoc.signal?.rule?.filters) ?? []; + const language = params.language ?? alertDoc.signal?.rule?.language ?? 'kuery'; + const query = params.query ?? alertDoc.signal?.rule?.query ?? ''; + const indexNames = params.index ?? alertDoc.signal?.rule?.index ?? []; + + const { thresholdFrom, thresholdTo, dataProviders } = getThresholdAggregationData(alertDoc); + return createTimeline({ + from: thresholdFrom, + notes: null, + timeline: { + ...timelineDefaults, + description: `_id: ${alertDoc._id}`, + filters: templateValues.filters ?? filters, + dataProviders: templateValues.dataProviders ?? dataProviders, + id: TimelineId.active, + indexNames, + dateRange: { + start: thresholdFrom, + end: thresholdTo, + }, + eventType: 'all', + kqlQuery: { + filterQuery: { + kuery: { + kind: language, + expression: templateValues.query ?? query, + }, + serializedQuery: templateValues.query ?? query, }, - serializedQuery: templateValues.query ?? query, }, }, - }, - to: thresholdTo, - ruleNote: noteContent, - }); + to: thresholdTo, + ruleNote: noteContent, + }); + } catch (error) { + const { toasts } = KibanaServices.get().notifications; + toasts.addError(error, { + toastMessage: i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.createThresholdTimelineFailure', + { + defaultMessage: 'Failed to create timeline for document _id: {id}', + values: { id: ecsData._id }, + } + ), + title: i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.createThresholdTimelineFailureTitle', + { + defaultMessage: 'Failed to create theshold alert timeline', + } + ), + }); + const from = DEFAULT_FROM_MOMENT.toISOString(); + const to = DEFAULT_TO_MOMENT.toISOString(); + return createTimeline({ + from, + notes: null, + timeline: { + ...timelineDefaults, + id: TimelineId.active, + indexNames: [], + dateRange: { + start: from, + end: to, + }, + eventType: 'all', + }, + to, + }); + } }; export const sendAlertToTimelineAction = async ({ @@ -492,7 +555,7 @@ export const sendAlertToTimelineAction = async ({ ); // threshold with template if (isThresholdRule(ecsData)) { - createThresholdTimeline(ecsData, createTimeline, noteContent, { + return createThresholdTimeline(ecsData, createTimeline, noteContent, { filters, query, dataProviders, @@ -550,7 +613,7 @@ export const sendAlertToTimelineAction = async ({ }); } } else if (isThresholdRule(ecsData)) { - createThresholdTimeline(ecsData, createTimeline, noteContent, {}); + return createThresholdTimeline(ecsData, createTimeline, noteContent, {}); } else { let { dataProviders, filters } = buildTimelineDataProviderOrFilter(alertIds ?? [], ecsData._id); if (isEqlRuleWithGroupId(ecsData)) { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.test.tsx new file mode 100644 index 0000000000000..24433e2f2ca99 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { fireEvent, render, act } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import { KibanaServices, useKibana } from '../../../../common/lib/kibana'; +import { Ecs } from '../../../../../common/ecs'; +import * as actions from '../actions'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import type { SendAlertToTimelineActionProps } from '../types'; +import { InvestigateInTimelineAction } from './investigate_in_timeline_action'; + +const ecsRowData: Ecs = { + _id: '1', + agent: { type: ['blah'] }, + kibana: { + alert: { + workflow_status: ['open'], + rule: { + parameters: {}, + uuid: ['testId'], + }, + }, + }, +}; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('../actions'); + +const props = { + ecsRowData, + onInvestigateInTimelineAlertClick: () => {}, + ariaLabel: 'test', +}; + +describe('use investigate in timeline hook', () => { + let mockSendAlertToTimeline: jest.SpyInstance, [SendAlertToTimelineActionProps]>; + + beforeEach(() => { + const coreStartMock = coreMock.createStart(); + (KibanaServices.get as jest.Mock).mockReturnValue(coreStartMock); + mockSendAlertToTimeline = jest.spyOn(actions, 'sendAlertToTimelineAction'); + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + search: { + searchStrategyClient: jest.fn(), + }, + query: jest.fn(), + }, + }, + }); + }); + afterEach(() => { + jest.resetAllMocks(); + }); + test('it creates a component and click handler', () => { + const wrapper = render( + + + + ); + expect(wrapper.getByTestId('send-alert-to-timeline-button')).toBeTruthy(); + }); + test('it calls sendAlertToTimelineAction once on click, not on mount', () => { + const wrapper = render( + + + + ); + expect(mockSendAlertToTimeline).toHaveBeenCalledTimes(0); + act(() => { + fireEvent.click(wrapper.getByTestId('send-alert-to-timeline-button')); + }); + expect(mockSendAlertToTimeline).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx index bca04dcf37a5b..b8d8232cb613c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx @@ -19,21 +19,18 @@ import { useInvestigateInTimeline } from './use_investigate_in_timeline'; interface InvestigateInTimelineActionProps { ecsRowData?: Ecs | Ecs[] | null; ariaLabel?: string; - alertIds?: string[]; buttonType?: 'text' | 'icon'; onInvestigateInTimelineAlertClick?: () => void; } const InvestigateInTimelineActionComponent: React.FC = ({ ariaLabel = ACTION_INVESTIGATE_IN_TIMELINE_ARIA_LABEL, - alertIds, ecsRowData, buttonType, onInvestigateInTimelineAlertClick, }) => { const { investigateInTimelineAlertClick } = useInvestigateInTimeline({ ecsRowData, - alertIds, onInvestigateInTimelineAlertClick, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx new file mode 100644 index 0000000000000..fc413a6f4f814 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.test.tsx @@ -0,0 +1,82 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { fireEvent, render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import { KibanaServices, useKibana } from '../../../../common/lib/kibana'; +import { Ecs } from '../../../../../common/ecs'; +import { useInvestigateInTimeline } from './use_investigate_in_timeline'; +import * as actions from '../actions'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import type { SendAlertToTimelineActionProps } from '../types'; + +const ecsRowData: Ecs = { + _id: '1', + agent: { type: ['blah'] }, + kibana: { + alert: { + workflow_status: ['open'], + rule: { + parameters: {}, + uuid: ['testId'], + }, + }, + }, +}; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('../actions'); + +const props = { + ecsRowData, + onInvestigateInTimelineAlertClick: () => {}, +}; + +describe('use investigate in timeline hook', () => { + let mockSendAlertToTimeline: jest.SpyInstance, [SendAlertToTimelineActionProps]>; + + beforeEach(() => { + const coreStartMock = coreMock.createStart(); + (KibanaServices.get as jest.Mock).mockReturnValue(coreStartMock); + mockSendAlertToTimeline = jest.spyOn(actions, 'sendAlertToTimelineAction'); + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + search: { + searchStrategyClient: jest.fn(), + }, + query: jest.fn(), + }, + }, + }); + }); + afterEach(() => { + jest.resetAllMocks(); + }); + test('it creates a component and click handler', () => { + const { result } = renderHook(() => useInvestigateInTimeline(props), { + wrapper: TestProviders, + }); + expect(result.current.investigateInTimelineActionItems).toBeTruthy(); + expect(typeof result.current.investigateInTimelineAlertClick).toBe('function'); + }); + + describe('the click handler calls createTimeline once and only once', () => { + test('runs 0 times on render, once on click', async () => { + const { result } = renderHook(() => useInvestigateInTimeline(props), { + wrapper: TestProviders, + }); + const component = result.current.investigateInTimelineActionItems[0]; + const { getByTestId } = render(component); + expect(mockSendAlertToTimeline).toHaveBeenCalledTimes(0); + act(() => { + fireEvent.click(getByTestId('investigate-in-timeline-action-item')); + }); + expect(mockSendAlertToTimeline).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx index c1cbe657415a6..301395eb5b963 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx @@ -6,32 +6,27 @@ */ import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; -import { isEmpty } from 'lodash'; import { EuiContextMenuItem } from '@elastic/eui'; import { useKibana } from '../../../../common/lib/kibana'; -import { TimelineId } from '../../../../../common/types/timeline'; +import { TimelineId, TimelineType } from '../../../../../common/types/timeline'; import { Ecs } from '../../../../../common/ecs'; -import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline'; import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline'; import { sendAlertToTimelineAction } from '../actions'; import { dispatchUpdateTimeline } from '../../../../timelines/components/open_timeline/helpers'; +import { useCreateTimeline } from '../../../../timelines/components/timeline/properties/use_create_timeline'; import { CreateTimelineProps } from '../types'; import { ACTION_INVESTIGATE_IN_TIMELINE } from '../translations'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; -import { useFetchEcsAlertsData } from '../../../containers/detection_engine/alerts/use_fetch_ecs_alerts_data'; interface UseInvestigateInTimelineActionProps { ecsRowData?: Ecs | Ecs[] | null; - nonEcsRowData?: TimelineNonEcsData[]; - alertIds?: string[] | null | undefined; onInvestigateInTimelineAlertClick?: () => void; } export const useInvestigateInTimeline = ({ ecsRowData, - alertIds, onInvestigateInTimelineAlertClick, }: UseInvestigateInTimelineActionProps) => { const { @@ -54,8 +49,14 @@ export const useInvestigateInTimeline = ({ [dispatch] ); + const clearActiveTimeline = useCreateTimeline({ + timelineId: TimelineId.active, + timelineType: TimelineType.default, + }); + const createTimeline = useCallback( ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => { + clearActiveTimeline(); updateTimelineIsLoading({ id: TimelineId.active, isLoading: false }); dispatchUpdateTimeline(dispatch)({ duplicate: true, @@ -72,27 +73,14 @@ export const useInvestigateInTimeline = ({ ruleNote, })(); }, - [dispatch, filterManager, updateTimelineIsLoading] + [dispatch, filterManager, updateTimelineIsLoading, clearActiveTimeline] ); - const showInvestigateInTimelineAction = alertIds != null; - const { isLoading: isFetchingAlertEcs, alertsEcsData } = useFetchEcsAlertsData({ - alertIds, - skip: alertIds == null, - }); - const investigateInTimelineAlertClick = useCallback(async () => { if (onInvestigateInTimelineAlertClick) { onInvestigateInTimelineAlertClick(); } - if (!isEmpty(alertsEcsData) && alertsEcsData !== null) { - await sendAlertToTimelineAction({ - createTimeline, - ecsData: alertsEcsData, - searchStrategyClient, - updateTimelineIsLoading, - }); - } else if (ecsRowData != null) { + if (ecsRowData != null) { await sendAlertToTimelineAction({ createTimeline, ecsData: ecsRowData, @@ -101,7 +89,6 @@ export const useInvestigateInTimeline = ({ }); } }, [ - alertsEcsData, createTimeline, ecsRowData, onInvestigateInTimelineAlertClick, @@ -109,22 +96,22 @@ export const useInvestigateInTimeline = ({ updateTimelineIsLoading, ]); - const investigateInTimelineActionItems = showInvestigateInTimelineAction - ? [ - - {ACTION_INVESTIGATE_IN_TIMELINE} - , - ] - : []; + const investigateInTimelineActionItems = useMemo( + () => [ + + {ACTION_INVESTIGATE_IN_TIMELINE} + , + ], + [ecsRowData, investigateInTimelineAlertClick] + ); return { investigateInTimelineActionItems, investigateInTimelineAlertClick, - showInvestigateInTimelineAction, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index d04f6c5d7d510..8ad76c70247bf 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -8,7 +8,6 @@ import React, { useState, useCallback, useMemo } from 'react'; import { EuiContextMenuPanel, EuiButton, EuiPopover } from '@elastic/eui'; import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; -import { isEmpty } from 'lodash/fp'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations'; import { useExceptionActions } from '../alerts_table/timeline_actions/use_add_exception_actions'; @@ -81,10 +80,6 @@ export const TakeActionDropdown = React.memo( [detailsData] ); - const alertIds = useMemo( - () => (isEmpty(actionsData.eventId) ? null : [actionsData.eventId]), - [actionsData.eventId] - ); const isEvent = actionsData.eventKind === 'event'; const isAgentEndpoint = useMemo(() => ecsData?.agent?.type?.includes('endpoint'), [ecsData]); @@ -156,7 +151,6 @@ export const TakeActionDropdown = React.memo( }); const { investigateInTimelineActionItems } = useInvestigateInTimeline({ - alertIds, ecsRowData: ecsData, onInvestigateInTimelineAlertClick: closePopoverHandler, }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data.ts deleted file mode 100644 index c459fab89a25e..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data.ts +++ /dev/null @@ -1,83 +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 { useEffect, useState } from 'react'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { isEmpty } from 'lodash'; - -import { Ecs } from '../../../../../common/ecs'; - -import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../common/constants'; -import { KibanaServices } from '../../../../common/lib/kibana'; -import { buildAlertsQuery, formatAlertToEcsSignal } from '../../../../common/utils/alerts'; - -export const useFetchEcsAlertsData = ({ - alertIds, - skip, - onError, -}: { - alertIds?: string[] | null | undefined; - skip?: boolean; - onError?: (e: Error) => void; -}): { isLoading: boolean | null; alertsEcsData: Ecs[] | null } => { - const [isLoading, setIsLoading] = useState(null); - const [alertsEcsData, setAlertEcsData] = useState(null); - - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - - const fetchAlert = async () => { - try { - setIsLoading(true); - const alertResponse = await KibanaServices.get().http.fetch< - estypes.SearchResponse<{ '@timestamp': string; [key: string]: unknown }> - >(DETECTION_ENGINE_QUERY_SIGNALS_URL, { - method: 'POST', - body: JSON.stringify(buildAlertsQuery(alertIds ?? [])), - }); - - setAlertEcsData( - alertResponse?.hits.hits.reduce( - (acc, { _id, _index, _source = {} }) => [ - ...acc, - { - ...formatAlertToEcsSignal(_source), - _id, - _index, - timestamp: _source['@timestamp'], - }, - ], - [] - ) ?? [] - ); - } catch (e) { - if (isSubscribed) { - if (onError) { - onError(e as Error); - } - } - } - if (isSubscribed) { - setIsLoading(false); - } - }; - - if (!isEmpty(alertIds) && !skip) { - fetchAlert(); - } - - return (): void => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, [alertIds, onError, skip]); - - return { - isLoading, - alertsEcsData, - }; -}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.test.tsx new file mode 100644 index 0000000000000..71d6f6253010d --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.test.tsx @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { render } from '@testing-library/react'; +import { EventDetailsFooter } from './footer'; +import '../../../../common/mock/match_media'; +import { TestProviders } from '../../../../common/mock'; +import { TimelineId } from '../../../../../common/types/timeline'; +import { Ecs } from '../../../../../common/ecs'; +import { mockAlertDetailsData } from '../../../../common/components/event_details/__mocks__'; +import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; +import { KibanaServices, useKibana } from '../../../../common/lib/kibana'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; + +const ecsData: Ecs = { + _id: '1', + agent: { type: ['blah'] }, + kibana: { + alert: { + workflow_status: ['open'], + rule: { + parameters: {}, + uuid: ['testId'], + }, + }, + }, +}; + +const mockAlertDetailsDataWithIsObject = mockAlertDetailsData.map((detail) => { + return { + ...detail, + isObjectArray: false, + }; +}) as TimelineEventsDetailsItem[]; + +jest.mock('../../../../../common/endpoint/service/host_isolation/utils', () => { + return { + isIsolationSupported: jest.fn().mockReturnValue(true), + }; +}); + +jest.mock( + '../../../../detections/containers/detection_engine/alerts/use_host_isolation_status', + () => { + return { + useHostIsolationStatus: jest.fn().mockReturnValue({ + loading: false, + isIsolated: false, + agentStatus: 'healthy', + }), + }; + } +); + +jest.mock('../../../../common/hooks/use_experimental_features', () => ({ + useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true), +})); + +jest.mock('../../../../detections/components/user_info', () => ({ + useUserData: jest.fn().mockReturnValue([{ canUserCRUD: true, hasIndexWrite: true }]), +})); +jest.mock('../../../../common/lib/kibana'); +jest.mock( + '../../../../detections/containers/detection_engine/alerts/use_alerts_privileges', + () => ({ + useAlertsPrivileges: jest.fn().mockReturnValue({ hasIndexWrite: true, hasKibanaCRUD: true }), + }) +); +jest.mock('../../../../cases/components/use_insert_timeline'); + +jest.mock('../../../../common/utils/endpoint_alert_check', () => { + return { + isAlertFromEndpointAlert: jest.fn().mockReturnValue(true), + isAlertFromEndpointEvent: jest.fn().mockReturnValue(true), + }; +}); +jest.mock( + '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline', + () => { + return { + useInvestigateInTimeline: jest.fn().mockReturnValue({ + investigateInTimelineActionItems: [
], + investigateInTimelineAlertClick: () => {}, + }), + }; + } +); +jest.mock('../../../../detections/components/alerts_table/actions'); + +const defaultProps = { + timelineId: TimelineId.test, + loadingEventDetails: false, + detailsEcsData: ecsData, + isHostIsolationPanelOpen: false, + handleOnEventClosed: jest.fn(), + onAddIsolationStatusClick: jest.fn(), + expandedEvent: { eventId: ecsData._id, indexName: '' }, + detailsData: mockAlertDetailsDataWithIsObject, +}; + +describe('event details footer component', () => { + beforeEach(() => { + const coreStartMock = coreMock.createStart(); + (KibanaServices.get as jest.Mock).mockReturnValue(coreStartMock); + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + search: { + searchStrategyClient: jest.fn(), + }, + query: jest.fn(), + }, + }, + }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + test('it renders the take action dropdown', () => { + const wrapper = render( + + + + ); + expect(wrapper.getByTestId('take-action-dropdown-btn')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx index 6f46111656871..86b23594c947a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useMemo } from 'react'; import { EuiFlyoutFooter, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { find, get, isEmpty } from 'lodash/fp'; +import { find } from 'lodash/fp'; import { connect, ConnectedProps } from 'react-redux'; import { TakeActionDropdown } from '../../../../detections/components/take_action_dropdown'; import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; @@ -19,7 +19,6 @@ import { useEventFilterModal } from '../../../../detections/components/alerts_ta import { getFieldValue } from '../../../../detections/components/host_isolation/helpers'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { Ecs } from '../../../../../common/ecs'; -import { useFetchEcsAlertsData } from '../../../../detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data'; import { inputsModel, inputsSelectors, State } from '../../../../common/store'; interface EventDetailsFooterProps { @@ -82,11 +81,6 @@ export const EventDetailsFooterComponent = React.memo( [detailsData] ); - const eventIds = useMemo( - () => (isEmpty(expandedEvent?.eventId) ? null : [expandedEvent?.eventId]), - [expandedEvent?.eventId] - ); - const refetchQuery = (newQueries: inputsModel.GlobalQuery[]) => { newQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); }; @@ -113,21 +107,15 @@ export const EventDetailsFooterComponent = React.memo( const { closeAddEventFilterModal, isAddEventFilterModalOpen, onAddEventFilterClick } = useEventFilterModal(); - const { alertsEcsData } = useFetchEcsAlertsData({ - alertIds: eventIds, - skip: expandedEvent?.eventId == null, - }); - - const ecsData = detailsEcsData ?? get(0, alertsEcsData); return ( <> - {ecsData && ( + {detailsEcsData && ( )} - {isAddEventFilterModalOpen && ecsData != null && ( - + {isAddEventFilterModalOpen && detailsEcsData != null && ( + )} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index 75ca399bf52d4..8be6200d1e84a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -48,7 +48,6 @@ const ActionsComponent: React.FC = ({ ariaRowindex, checked, columnValues, - data, ecsData, eventId, eventIdToNoteIds, @@ -68,7 +67,6 @@ const ActionsComponent: React.FC = ({ const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); const emptyNotes: string[] = []; const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const alertIds = useMemo(() => [ecsData._id], [ecsData]); const onPinEvent: OnPinEvent = useCallback( (evtId) => dispatch(timelineActions.pinEvent({ id: timelineId, eventId: evtId })), @@ -167,7 +165,6 @@ const ActionsComponent: React.FC = ({ )} diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx index 77a761edebd49..b1a16cf5b3abd 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx @@ -7,7 +7,6 @@ import { EuiCheckbox, EuiLoadingSpinner } from '@elastic/eui'; import React, { useCallback } from 'react'; -import { ALERT_RULE_PRODUCER } from '@kbn/rule-data-utils'; import type { ActionProps, HeaderActionProps } from '../../../../../common/types'; import * as i18n from './translations'; @@ -19,10 +18,7 @@ export const RowCheckBox = ({ columnValues, disabled, loadingEventIds, - data, }: ActionProps) => { - const ruleProducers = data.find((d) => d.field === ALERT_RULE_PRODUCER)?.value ?? []; - const ruleProducer = ruleProducers[0]; const handleSelectEvent = useCallback( (event: React.ChangeEvent) => { if (!disabled) { @@ -39,7 +35,7 @@ export const RowCheckBox = ({ ) : ( Date: Tue, 8 Feb 2022 13:36:19 +0100 Subject: [PATCH 19/44] [Lens] fix Formula to Quick functions does not preserve custom formatting (#124840) * [Lens] fix transitioning from Formula to Quick functions does not preserve chosen format * linter --- .../operations/layer_helpers.test.ts | 45 +++++++++++++++++++ .../operations/layer_helpers.ts | 1 + 2 files changed, 46 insertions(+) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index dad1250b39e14..1b432c4a34add 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -2119,6 +2119,51 @@ describe('state_helpers', () => { }, }); }); + + it('should carry over a custom formatting when transitioning from a managed reference', () => { + const actual = replaceColumn({ + layer: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + label: 'MY CUSTOM LABEL', + customLabel: true, + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { + isFormulaBroken: false, + formula: 'average(bytes)', + format: { + id: 'number', + params: { decimals: 2 }, + }, + }, + references: [], + } as FormulaIndexPatternColumn, + }, + }, + indexPattern, + columnId: 'col1', + op: 'average', + field: indexPattern.fields[2], // bytes field + visualizationGroups: [], + shouldResetLabel: undefined, + }); + + expect(actual.columns.col1).toEqual( + expect.objectContaining({ + params: { + format: { + id: 'number', + params: { decimals: 2 }, + }, + }, + }) + ); + }); }); it('should allow making a replacement on an operation that is being referenced, even if it ends up invalid', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 7985500798b38..438d728b7df1f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -422,6 +422,7 @@ export function replaceColumn({ op, field, visualizationGroups, + incompleteParams: previousColumn, }); // if the formula label is not the default one, propagate it to the new operation From 3f702c1fd46ce408daad24295eaee0c61ed7a5af Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 8 Feb 2022 14:33:53 +0100 Subject: [PATCH 20/44] [Lens] Make top values work for custom numeric formatters (#124566) * :bug: Make top values work with custom formatter * :bug: Make custom formatter work with multi-terms formatter * :bug: simplify parentFormat logic and add more tests * :ok_hand: Integrate feedback * :sparkles: Add migration for top values formatting Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../format_column/format_column.test.ts | 30 +++ .../format_column/format_column_fn.ts | 38 +++- .../droppable/droppable.test.ts | 3 + .../operations/definitions/terms/index.tsx | 29 ++- .../definitions/terms/terms.test.tsx | 176 ++++++++++++++++-- .../operations/definitions/terms/types.ts | 4 - .../server/migrations/common_migrations.ts | 31 +++ .../saved_object_migrations.test.ts | 75 ++++++++ .../migrations/saved_object_migrations.ts | 8 +- .../plugins/lens/server/migrations/types.ts | 31 ++- 10 files changed, 399 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts b/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts index 4d53f96c71fd8..17192103efaae 100644 --- a/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts +++ b/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts @@ -159,6 +159,36 @@ describe('format_column', () => { params: { pattern: '0,0.00000', }, + pattern: '0,0.00000', + }, + }); + }); + + it('should support multi-fields formatters', async () => { + datatable.columns[0].meta.params = { + id: 'previousWrapper', + params: { id: 'myMultiFieldFormatter', paramsPerField: [{ id: 'number' }] }, + }; + const result = await fn(datatable, { + columnId: 'test', + format: 'number', + decimals: 5, + parentFormat: JSON.stringify({ id: 'wrapper', params: { wrapperParam: 123 } }), + }); + expect(result.columns[0].meta.params).toEqual({ + id: 'wrapper', + params: { + wrapperParam: 123, + id: 'number', + paramsPerField: [ + { + id: 'number', + params: { + pattern: '0,0.00000', + }, + pattern: '0,0.00000', + }, + ], }, }); }); diff --git a/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts b/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts index 37540ee0950af..93b54e777b645 100644 --- a/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts +++ b/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts @@ -43,18 +43,48 @@ export const formatColumnFn: FormatColumnExpressionFunction['fn'] = ( const parentFormatId = parsedParentFormat.id; const parentFormatParams = parsedParentFormat.params ?? {}; - if (!parentFormatId) { + // Be careful here to check for undefined custom format + const isDuplicateParentFormatter = parentFormatId === col.meta.params?.id && format == null; + if (!parentFormatId || isDuplicateParentFormatter) { return col; } if (format && supportedFormats[format]) { + const customParams = { + pattern: supportedFormats[format].decimalsToPattern(decimals), + }; + // Some parent formatters are multi-fields and wrap the custom format into a "paramsPerField" + // property. Here the format is passed to this property to make it work properly + if (col.meta.params?.params?.paramsPerField?.length) { + return withParams(col, { + id: parentFormatId, + params: { + ...col.meta.params?.params, + id: format, + ...parentFormatParams, + // some wrapper formatters require params to be flatten out (i.e. terms) while others + // require them to be in the params property (i.e. ranges) + // so for now duplicate + paramsPerField: col.meta.params?.params?.paramsPerField.map( + (f: { id: string | undefined; params: Record | undefined }) => ({ + ...f, + params: { ...f.params, ...customParams }, + ...customParams, + }) + ), + }, + }); + } return withParams(col, { id: parentFormatId, params: { + ...col.meta.params?.params, id: format, - params: { - pattern: supportedFormats[format].decimalsToPattern(decimals), - }, + // some wrapper formatters require params to be flatten out (i.e. terms) while others + // require them to be in the params property (i.e. ranges) + // so for now duplicate + ...customParams, + params: customParams, ...parentFormatParams, }, }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts index 3871715cf31e5..002fec786d7e6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts @@ -2167,6 +2167,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, orderDirection: 'desc', size: 10, + parentFormat: { id: 'terms' }, }, }, col3: testState.layers.first.columns.col3, @@ -2257,6 +2258,7 @@ describe('IndexPatternDimensionEditorPanel', () => { type: 'alphabetical', }, orderDirection: 'desc', + parentFormat: { id: 'terms' }, size: 10, }, }, @@ -2267,6 +2269,7 @@ describe('IndexPatternDimensionEditorPanel', () => { filter: undefined, operationType: 'unique_count', sourceField: 'src', + timeShift: undefined, dataType: 'number', params: undefined, scale: 'ratio', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index be286532ad75b..4c656d15f197f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -81,6 +81,11 @@ function isScriptedField(fieldName: string | IndexPatternField, indexPattern?: I return fieldName.scripted; } +// It is not always possible to know if there's a numeric field, so just ignore it for now +function getParentFormatter(params: Partial) { + return { id: params.secondaryFields?.length ? 'multi_terms' : 'terms' }; +} + const idPrefix = htmlIdGenerator()(); const DEFAULT_SIZE = 3; // Elasticsearch limit @@ -124,9 +129,18 @@ export const termsOperation: OperationDefinition targetColumn.sourceField !== f), + // remove the sourceField + secondaryFields.delete(targetColumn.sourceField); + + const secondaryFieldsList: string[] = [...secondaryFields]; + const ret: Partial = { + secondaryFields: secondaryFieldsList, + parentFormat: getParentFormatter({ + ...targetColumn.params, + secondaryFields: secondaryFieldsList, + }), }; + return ret; }, canAddNewField: ({ targetColumn, sourceColumn, field, indexPattern }) => { // first step: collect the fields from the targetColumn @@ -222,6 +236,7 @@ export const termsOperation: OperationDefinition { + (fields: string[]) => { const column = layer.columns[columnId] as TermsIndexPatternColumn; + const secondaryFields = fields.length > 1 ? fields.slice(1) : undefined; updateLayer({ ...layer, columns: { @@ -364,7 +381,11 @@ export const termsOperation: OperationDefinition 1 ? fields.slice(1) : undefined, + secondaryFields, + parentFormat: getParentFormatter({ + ...column.params, + secondaryFields, + }), }, }, } as Record, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index fb3943289437d..cfdab76c9b6d9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { shallow, mount } from 'enzyme'; -import { EuiButtonGroup, EuiFieldNumber, EuiSelect, EuiSwitch } from '@elastic/eui'; +import { EuiButtonGroup, EuiComboBox, EuiFieldNumber, EuiSelect, EuiSwitch } from '@elastic/eui'; import type { IUiSettingsClient, SavedObjectsClientContract, @@ -39,6 +39,16 @@ jest.mock('@elastic/eui', () => { }; }); +// Need to mock the debounce call to test some FieldInput behaviour +jest.mock('lodash', () => { + const original = jest.requireActual('lodash'); + + return { + ...original, + debounce: (fn: unknown) => fn, + }; +}); + const uiSettingsMock = {} as IUiSettingsClient; const defaultProps = { @@ -224,15 +234,18 @@ describe('terms', () => { }, orderDirection: 'asc', format: { id: 'number', params: { decimals: 0 } }, + parentFormat: { id: 'terms' }, }, }; const indexPattern = createMockedIndexPattern(); - const newStringField = indexPattern.fields.find((i) => i.name === 'source')!; + const newStringField = indexPattern.getFieldByName('source')!; const column = termsOperation.onFieldChange(oldColumn, newStringField); expect(column).toHaveProperty('dataType', 'string'); expect(column).toHaveProperty('sourceField', 'source'); expect(column.params.format).toBeUndefined(); + // Preserve the parentFormat as it will be ignored down the line if not required + expect(column.params.parentFormat).toEqual({ id: 'terms' }); }); it('should remove secondary fields when a new field is passed', () => { @@ -253,7 +266,7 @@ describe('terms', () => { }, }; const indexPattern = createMockedIndexPattern(); - const newStringField = indexPattern.fields.find((i) => i.name === 'source')!; + const newStringField = indexPattern.getFieldByName('source')!; const column = termsOperation.onFieldChange(oldColumn, newStringField); expect(column.params.secondaryFields).toBeUndefined(); @@ -277,13 +290,59 @@ describe('terms', () => { }, }; const indexPattern = createMockedIndexPattern(); - const sanemStringField = indexPattern.fields.find((i) => i.name === 'bytes')!; + const newNumericField = indexPattern.getFieldByName('bytes')!; - const column = termsOperation.onFieldChange(oldColumn, sanemStringField, { + const column = termsOperation.onFieldChange(oldColumn, newNumericField, { secondaryFields: ['dest', 'geo.src'], }); expect(column.params.secondaryFields).toEqual(expect.arrayContaining(['dest', 'geo.src'])); }); + + it('should reassign the parentFormatter on single field change', () => { + const oldColumn: TermsIndexPatternColumn = { + operationType: 'terms', + sourceField: 'bytes', + label: 'Top values of bytes', + isBucketed: true, + dataType: 'number', + params: { + size: 5, + orderBy: { + type: 'alphabetical', + }, + orderDirection: 'asc', + format: { id: 'number', params: { decimals: 0 } }, + }, + }; + const indexPattern = createMockedIndexPattern(); + const newNumberField = indexPattern.getFieldByName('memory')!; + + const column = termsOperation.onFieldChange(oldColumn, newNumberField); + expect(column.params.parentFormat).toEqual({ id: 'terms' }); + }); + + it('should reassign the parentFormatter on multiple fields change', () => { + const oldColumn: TermsIndexPatternColumn = { + operationType: 'terms', + sourceField: 'bytes', + label: 'Top values of bytes', + isBucketed: true, + dataType: 'number', + params: { + size: 5, + orderBy: { + type: 'alphabetical', + }, + orderDirection: 'asc', + format: { id: 'number', params: { decimals: 0 } }, + }, + }; + const indexPattern = createMockedIndexPattern(); + const newNumberField = indexPattern.getFieldByName('memory')!; + + const column = termsOperation.onFieldChange(oldColumn, newNumberField); + expect(column.params.parentFormat).toEqual({ id: 'terms' }); + }); }); describe('getPossibleOperationForField', () => { @@ -516,6 +575,23 @@ describe('terms', () => { }); expect(termsColumn.params).toEqual(expect.objectContaining({ size: 5 })); }); + + it('should set a parentFormat as "terms" if a numeric field is passed', () => { + const termsColumn = termsOperation.buildColumn({ + indexPattern: createMockedIndexPattern(), + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, + field: { + aggregatable: true, + searchable: true, + type: 'number', + name: 'numericTest', + displayName: 'test', + }, + }); + expect(termsColumn.params).toEqual( + expect.objectContaining({ parentFormat: { id: 'terms' } }) + ); + }); }); describe('onOtherColumnChanged', () => { @@ -1364,6 +1440,47 @@ describe('terms', () => { instance.find('[data-test-subj="indexPattern-terms-add-field"]').first().prop('isDisabled') ).toBeTruthy(); }); + + it('should update the parentFormatter on transition between single to multi terms', () => { + const updateLayerSpy = jest.fn(); + const existingFields = getExistingFields(); + const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields); + + let instance = mount( + + ); + // add a new field + act(() => { + instance.find('[data-test-subj="indexPattern-terms-add-field"]').first().simulate('click'); + }); + instance = instance.update(); + + act(() => { + instance.find(EuiComboBox).last().prop('onChange')!([ + { value: { type: 'field', field: 'bytes' }, label: 'bytes' }, + ]); + }); + + expect(updateLayerSpy).toHaveBeenCalledWith( + expect.objectContaining({ + columns: expect.objectContaining({ + col1: expect.objectContaining({ + params: expect.objectContaining({ + parentFormat: { id: 'multi_terms' }, + }), + }), + }), + }) + ); + }); }); describe('param editor', () => { @@ -2046,7 +2163,10 @@ describe('terms', () => { sourceColumn: createMultiTermsColumn(['bytes', 'memory']), indexPattern: defaultProps.indexPattern, }) - ).toEqual({ secondaryFields: expect.arrayContaining(['dest', 'bytes', 'memory']) }); + ).toEqual({ + secondaryFields: expect.arrayContaining(['dest', 'bytes', 'memory']), + parentFormat: { id: 'multi_terms' }, + }); }); it('should return existing multiterms with only new fields from source column', () => { @@ -2056,7 +2176,10 @@ describe('terms', () => { sourceColumn: createMultiTermsColumn(['bytes', 'dest']), indexPattern: defaultProps.indexPattern, }) - ).toEqual({ secondaryFields: expect.arrayContaining(['dest', 'bytes']) }); + ).toEqual({ + secondaryFields: expect.arrayContaining(['dest', 'bytes']), + parentFormat: { id: 'multi_terms' }, + }); }); it('should return existing multiterms with only multiple new fields from source column', () => { @@ -2066,7 +2189,10 @@ describe('terms', () => { sourceColumn: createMultiTermsColumn(['dest', 'bytes', 'memory']), indexPattern: defaultProps.indexPattern, }) - ).toEqual({ secondaryFields: expect.arrayContaining(['dest', 'bytes', 'memory']) }); + ).toEqual({ + secondaryFields: expect.arrayContaining(['dest', 'bytes', 'memory']), + parentFormat: { id: 'multi_terms' }, + }); }); it('should append field to multiterms', () => { @@ -2079,7 +2205,10 @@ describe('terms', () => { field, indexPattern, }) - ).toEqual({ secondaryFields: expect.arrayContaining(['bytes']) }); + ).toEqual({ + secondaryFields: expect.arrayContaining(['bytes']), + parentFormat: { id: 'multi_terms' }, + }); }); it('should not append scripted field to multiterms', () => { @@ -2092,7 +2221,7 @@ describe('terms', () => { field, indexPattern, }) - ).toEqual({ secondaryFields: [] }); + ).toEqual({ secondaryFields: [], parentFormat: { id: 'terms' } }); }); it('should add both sourceColumn and field (as last term) to the targetColumn', () => { @@ -2105,7 +2234,10 @@ describe('terms', () => { field, indexPattern, }) - ).toEqual({ secondaryFields: expect.arrayContaining(['dest', 'memory', 'bytes']) }); + ).toEqual({ + secondaryFields: expect.arrayContaining(['dest', 'memory', 'bytes']), + parentFormat: { id: 'multi_terms' }, + }); }); it('should not add sourceColumn field if it has only scripted field', () => { @@ -2118,7 +2250,27 @@ describe('terms', () => { field, indexPattern, }) - ).toEqual({ secondaryFields: expect.arrayContaining(['dest', 'bytes']) }); + ).toEqual({ + secondaryFields: expect.arrayContaining(['dest', 'bytes']), + parentFormat: { id: 'multi_terms' }, + }); + }); + + it('should assign a parent formatter if a custom formatter is present', () => { + const indexPattern = createMockedIndexPattern(); + + const targetColumn = createMultiTermsColumn(['source', 'dest']); + targetColumn.params.format = { id: 'bytes', params: { decimals: 2 } }; + expect( + termsOperation.getParamsForMultipleFields?.({ + targetColumn, + sourceColumn: createMultiTermsColumn(['scripted']), + indexPattern, + }) + ).toEqual({ + secondaryFields: expect.arrayContaining(['dest']), + parentFormat: { id: 'multi_terms' }, + }); }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/types.ts index 584893f182666..1284870327653 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/types.ts @@ -30,10 +30,6 @@ export interface TermsIndexPatternColumn extends FieldBasedIndexPatternColumn { }; parentFormat?: { id: string; - params?: { - id?: string; - template?: string; - }; }; }; } diff --git a/x-pack/plugins/lens/server/migrations/common_migrations.ts b/x-pack/plugins/lens/server/migrations/common_migrations.ts index 87edc94fd1ae6..39eed3cbc2a35 100644 --- a/x-pack/plugins/lens/server/migrations/common_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/common_migrations.ts @@ -250,3 +250,34 @@ export const getLensFilterMigrations = ( state: { ...lensDoc.attributes.state, filters: migrate(lensDoc.attributes.state.filters) }, }, })); + +export const fixLensTopValuesCustomFormatting = (attributes: LensDocShape810): LensDocShape810 => { + const newAttributes = cloneDeep(attributes); + const datasourceLayers = newAttributes.state.datasourceStates.indexpattern.layers || {}; + (newAttributes as LensDocShape810).state.datasourceStates.indexpattern.layers = + Object.fromEntries( + Object.entries(datasourceLayers).map(([layerId, layer]) => { + return [ + layerId, + { + ...layer, + columns: Object.fromEntries( + Object.entries(layer.columns).map(([columnId, column]) => { + if (column.operationType === 'terms') { + return [ + columnId, + { + ...column, + params: { ...column.params, parentFormat: { id: 'terms' } }, + }, + ]; + } + return [columnId, column]; + }) + ), + }, + ]; + }) + ); + return newAttributes as LensDocShape810; +}; diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts index 11572b7a1f7d9..5cd63b2786fe4 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts @@ -1704,4 +1704,79 @@ describe('Lens migrations', () => { }, }); }); + + describe('8.1.0 add parentFormat to terms operation', () => { + const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext; + const example = { + type: 'lens', + id: 'mocked-saved-object-id', + attributes: { + savedObjectId: '1', + title: 'MyRenamedOps', + description: '', + visualizationType: null, + state: { + datasourceMetaData: { + filterableIndexPatterns: [], + }, + datasourceStates: { + indexpattern: { + currentIndexPatternId: 'logstash-*', + layers: { + '2': { + columns: { + '3': { + dataType: 'string', + isBucketed: true, + label: 'Top values of geoip.country_iso_code', + operationType: 'terms', + params: {}, + scale: 'ordinal', + sourceField: 'geoip.country_iso_code', + }, + '4': { + label: 'Anzahl der Aufnahmen', + dataType: 'number', + operationType: 'count', + sourceField: 'Aufnahmen', + isBucketed: false, + scale: 'ratio', + }, + '5': { + label: 'Sum of bytes', + dataType: 'numver', + operationType: 'sum', + sourceField: 'bytes', + isBucketed: false, + scale: 'ratio', + }, + }, + columnOrder: ['3', '4', '5'], + }, + }, + }, + }, + visualization: {}, + query: { query: '', language: 'kuery' }, + filters: [], + }, + }, + } as unknown as SavedObjectUnsanitizedDoc; + + it('should change field for count operations but not for others, not changing the vis', () => { + const result = migrations['8.1.0'](example, context) as ReturnType< + SavedObjectMigrationFn + >; + + expect( + Object.values( + result.attributes.state.datasourceStates.indexpattern.layers['2'].columns + ).find(({ operationType }) => operationType === 'terms') + ).toEqual( + expect.objectContaining({ + params: expect.objectContaining({ parentFormat: { id: 'terms' } }), + }) + ); + }); + }); }); diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts index 8e7d555b33694..2617fb42bce09 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts @@ -39,6 +39,7 @@ import { getLensFilterMigrations, getLensCustomVisualizationMigrations, commonRenameRecordsField, + fixLensTopValuesCustomFormatting, } from './common_migrations'; interface LensDocShapePre710 { @@ -458,6 +459,11 @@ const renameRecordsField: SavedObjectMigrationFn = (doc) => { + const newDoc = cloneDeep(doc); + return { ...newDoc, attributes: fixLensTopValuesCustomFormatting(newDoc.attributes) }; +}; + const lensMigrations: SavedObjectMigrationMap = { '7.7.0': removeInvalidAccessors, // The order of these migrations matter, since the timefield migration relies on the aggConfigs @@ -471,7 +477,7 @@ const lensMigrations: SavedObjectMigrationMap = { '7.14.0': removeTimezoneDateHistogramParam, '7.15.0': addLayerTypeToVisualization, '7.16.0': moveDefaultReversedPaletteToCustom, - '8.1.0': flow(renameFilterReferences, renameRecordsField), + '8.1.0': flow(renameFilterReferences, renameRecordsField, addParentFormatter), }; export const getAllMigrations = ( diff --git a/x-pack/plugins/lens/server/migrations/types.ts b/x-pack/plugins/lens/server/migrations/types.ts index 11c23d98dab37..7cbb2052dbfff 100644 --- a/x-pack/plugins/lens/server/migrations/types.ts +++ b/x-pack/plugins/lens/server/migrations/types.ts @@ -200,9 +200,38 @@ export interface LensDocShape715 { export type LensDocShape810 = Omit< LensDocShape715, - 'filters' + 'filters' | 'state' > & { filters: Filter[]; + state: Omit & { + datasourceStates: { + indexpattern: Omit & { + layers: Record< + string, + Omit< + LensDocShape715['state']['datasourceStates']['indexpattern']['layers'][string], + 'columns' + > & { + columns: Record< + string, + | { + operationType: 'terms'; + params: { + secondaryFields?: string[]; + }; + [key: string]: unknown; + } + | { + operationType: OperationTypePost712; + params: Record; + [key: string]: unknown; + } + >; + } + >; + }; + }; + }; }; export type VisState716 = From ca9c004f1a247b018f1e4b38fcab5e4a5e988b4f Mon Sep 17 00:00:00 2001 From: Tobias Stadler Date: Tue, 8 Feb 2022 15:04:25 +0100 Subject: [PATCH 21/44] Always allow internal urls in Vega (#124705) --- ...-core-public.iexternalurl.isinternalurl.md | 24 +++++++++++++++++++ .../kibana-plugin-core-public.iexternalurl.md | 1 + .../public/http/external_url_service.test.ts | 17 +++++++++++++ src/core/public/http/external_url_service.ts | 24 +++++++++++++++---- src/core/public/http/http_service.mock.ts | 1 + src/core/public/http/types.ts | 7 ++++++ src/core/public/public.api.md | 1 + .../dashboard_empty_screen.test.tsx.snap | 3 +++ .../__snapshots__/home.test.tsx.snap | 3 +++ .../__snapshots__/flyout.test.tsx.snap | 1 + ...telemetry_management_section.test.tsx.snap | 1 + .../vega/public/vega_view/vega_base_view.js | 2 +- .../public/lib/url_drilldown.test.ts | 4 ++++ 13 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.iexternalurl.isinternalurl.md diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurl.isinternalurl.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurl.isinternalurl.md new file mode 100644 index 0000000000000..396e5586f1fed --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurl.isinternalurl.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExternalUrl](./kibana-plugin-core-public.iexternalurl.md) > [isInternalUrl](./kibana-plugin-core-public.iexternalurl.isinternalurl.md) + +## IExternalUrl.isInternalUrl() method + +Determines if the provided URL is an internal url. + +Signature: + +```typescript +isInternalUrl(relativeOrAbsoluteUrl: string): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| relativeOrAbsoluteUrl | string | | + +Returns: + +boolean + diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurl.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurl.md index 5a598281c7be7..d0d4e6a3a4464 100644 --- a/docs/development/core/public/kibana-plugin-core-public.iexternalurl.md +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurl.md @@ -16,5 +16,6 @@ export interface IExternalUrl | Method | Description | | --- | --- | +| [isInternalUrl(relativeOrAbsoluteUrl)](./kibana-plugin-core-public.iexternalurl.isinternalurl.md) | Determines if the provided URL is an internal url. | | [validateUrl(relativeOrAbsoluteUrl)](./kibana-plugin-core-public.iexternalurl.validateurl.md) | Determines if the provided URL is a valid location to send users. Validation is based on the configured allow list in kibana.yml.If the URL is valid, then a URL will be returned. Otherwise, this will return null. | diff --git a/src/core/public/http/external_url_service.test.ts b/src/core/public/http/external_url_service.test.ts index ee757c5046760..4ce3709ff6366 100644 --- a/src/core/public/http/external_url_service.test.ts +++ b/src/core/public/http/external_url_service.test.ts @@ -73,6 +73,23 @@ const internalRequestScenarios = [ ]; describe('External Url Service', () => { + describe('#isInternalUrl', () => { + const { setup } = setupService({ + location: new URL('https://example.com/app/management?q=1&bar=false#some-hash'), + serverBasePath: '', + policy: [], + }); + + it('internal request', () => { + expect(setup.isInternalUrl('/')).toBeTruthy(); + expect(setup.isInternalUrl('https://example.com/')).toBeTruthy(); + }); + + it('external request', () => { + expect(setup.isInternalUrl('https://elastic.co/')).toBeFalsy(); + }); + }); + describe('#validateUrl', () => { describe('internal requests with a server base path', () => { const serverBasePath = '/base-path'; diff --git a/src/core/public/http/external_url_service.ts b/src/core/public/http/external_url_service.ts index 166e167b3b994..0fb1c85d48257 100644 --- a/src/core/public/http/external_url_service.ts +++ b/src/core/public/http/external_url_service.ts @@ -50,20 +50,33 @@ function normalizeProtocol(protocol: string) { return protocol.endsWith(':') ? protocol.slice(0, -1).toLowerCase() : protocol.toLowerCase(); } +const createIsInternalUrlValidation = ( + location: Pick, + serverBasePath: string +) => { + return function isInternallUrl(next: string) { + const base = new URL(location.href); + const url = new URL(next, base); + + return ( + url.origin === base.origin && + (!serverBasePath || url.pathname.startsWith(`${serverBasePath}/`)) + ); + }; +}; + const createExternalUrlValidation = ( rules: IExternalUrlPolicy[], location: Pick, serverBasePath: string ) => { + const isInternalUrl = createIsInternalUrlValidation(location, serverBasePath); + return function validateExternalUrl(next: string) { const base = new URL(location.href); const url = new URL(next, base); - const isInternalURL = - url.origin === base.origin && - (!serverBasePath || url.pathname.startsWith(`${serverBasePath}/`)); - - if (isInternalURL) { + if (isInternalUrl(next)) { return url; } @@ -90,6 +103,7 @@ export class ExternalUrlService implements CoreService { const { policy } = injectedMetadata.getExternalUrlConfig(); return { + isInternalUrl: createIsInternalUrlValidation(location, serverBasePath), validateUrl: createExternalUrlValidation(policy, location, serverBasePath), }; } diff --git a/src/core/public/http/http_service.mock.ts b/src/core/public/http/http_service.mock.ts index fff99d84a76a6..bfd81a1003736 100644 --- a/src/core/public/http/http_service.mock.ts +++ b/src/core/public/http/http_service.mock.ts @@ -36,6 +36,7 @@ const createServiceMock = ({ isAnonymous: jest.fn(), }, externalUrl: { + isInternalUrl: jest.fn(), validateUrl: jest.fn(), }, addLoadingCountSource: jest.fn(), diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index 876799765ea1c..afe1d653c599c 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -110,6 +110,13 @@ export interface IBasePath { * @public */ export interface IExternalUrl { + /** + * Determines if the provided URL is an internal url. + * + * @param relativeOrAbsoluteUrl + */ + isInternalUrl(relativeOrAbsoluteUrl: string): boolean; + /** * Determines if the provided URL is a valid location to send users. * Validation is based on the configured allow list in kibana.yml. diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index db132e267807e..d8cf4706ceb16 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -686,6 +686,7 @@ export interface IBasePath { // @public export interface IExternalUrl { + isInternalUrl(relativeOrAbsoluteUrl: string): boolean; validateUrl(relativeOrAbsoluteUrl: string): URL | null; } diff --git a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap index 70a21438754bd..e416dced4f8a1 100644 --- a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -19,6 +19,7 @@ exports[`DashboardEmptyScreen renders correctly with edit mode 1`] = ` }, "delete": [MockFunction], "externalUrl": Object { + "isInternalUrl": [MockFunction], "validateUrl": [MockFunction], }, "fetch": [MockFunction], @@ -343,6 +344,7 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` }, "delete": [MockFunction], "externalUrl": Object { + "isInternalUrl": [MockFunction], "validateUrl": [MockFunction], }, "fetch": [MockFunction], @@ -706,6 +708,7 @@ exports[`DashboardEmptyScreen renders correctly with view mode 1`] = ` }, "delete": [MockFunction], "externalUrl": Object { + "isInternalUrl": [MockFunction], "validateUrl": [MockFunction], }, "fetch": [MockFunction], diff --git a/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap b/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap index 373fc8ea59b6f..ab6ad1b6cc0c5 100644 --- a/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap +++ b/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap @@ -396,6 +396,7 @@ exports[`home isNewKibanaInstance should set isNewKibanaInstance to true when th }, "delete": [MockFunction], "externalUrl": Object { + "isInternalUrl": [MockFunction], "validateUrl": [MockFunction], }, "fetch": [MockFunction], @@ -464,6 +465,7 @@ exports[`home isNewKibanaInstance should set isNewKibanaInstance to true when th }, "delete": [MockFunction], "externalUrl": Object { + "isInternalUrl": [MockFunction], "validateUrl": [MockFunction], }, "fetch": [MockFunction], @@ -533,6 +535,7 @@ exports[`home isNewKibanaInstance should set isNewKibanaInstance to true when th }, "delete": [MockFunction], "externalUrl": Object { + "isInternalUrl": [MockFunction], "validateUrl": [MockFunction], }, "fetch": [MockFunction], diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap index 9651b8658032e..c816bbe65d4f1 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -182,6 +182,7 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` }, "delete": [MockFunction], "externalUrl": Object { + "isInternalUrl": [MockFunction], "validateUrl": [MockFunction], }, "fetch": [MockFunction], diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap index 0edad23d3312b..cccbd07fe14b3 100644 --- a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap +++ b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap @@ -280,6 +280,7 @@ exports[`TelemetryManagementSectionComponent renders null because allowChangingO }, "delete": [MockFunction], "externalUrl": Object { + "isInternalUrl": [MockFunction], "validateUrl": [MockFunction], }, "fetch": [MockFunction], diff --git a/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js b/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js index ce6454ef0a0e1..a87c8318e319c 100644 --- a/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_types/vega/public/vega_view/vega_base_view.js @@ -207,7 +207,7 @@ export class VegaBaseView { const vegaLoader = loader(); const originalSanitize = vegaLoader.sanitize.bind(vegaLoader); vegaLoader.sanitize = async (uri, options) => { - if (uri.bypassToken === bypassToken) { + if (uri.bypassToken === bypassToken || this._externalUrl.isInternalUrl(uri)) { // If uri has a bypass token, the uri was encoded by bypassExternalUrlCheck() above. // because user can only supply pure JSON data structure. uri = uri.url; diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts index c4eee1282de87..c8d187549a66b 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts @@ -68,6 +68,10 @@ const mockNavigateToUrl = jest.fn(() => Promise.resolve()); class TextExternalUrl implements IExternalUrl { constructor(private readonly isCorrect: boolean = true) {} + public isInternalUrl(url: string): boolean { + return false; + } + public validateUrl(url: string): URL | null { return this.isCorrect ? new URL(url) : null; } From 815685f26487dc668595527a7156e722e12a142e Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 8 Feb 2022 17:05:16 +0300 Subject: [PATCH 22/44] [TSVB] Lucene query on dashboard level is not respected for annotations request (#124802) Closes: #124693 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../annotations/query.test.ts | 439 ++++++++++++++++++ .../request_processors/annotations/query.ts | 23 +- .../request_processors/table/query.test.ts | 340 ++++++++++++++ .../request_processors/table/query.ts | 13 +- 4 files changed, 796 insertions(+), 19 deletions(-) create mode 100644 src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/query.test.ts create mode 100644 src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/query.test.ts diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/query.test.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/query.test.ts new file mode 100644 index 0000000000000..83271d492718b --- /dev/null +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/query.test.ts @@ -0,0 +1,439 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { query } from './query'; + +import type { + AnnotationsRequestProcessorsFunction, + AnnotationsRequestProcessorsParams, +} from './types'; +import { DefaultSearchCapabilities } from '../../../search_strategies/capabilities/default_search_capabilities'; + +describe('query', () => { + let req: AnnotationsRequestProcessorsParams['req']; + let panel: AnnotationsRequestProcessorsParams['panel']; + let annotation: AnnotationsRequestProcessorsParams['annotation']; + let esQueryConfig: AnnotationsRequestProcessorsParams['esQueryConfig']; + let annotationIndex: AnnotationsRequestProcessorsParams['annotationIndex']; + let capabilities: AnnotationsRequestProcessorsParams['capabilities']; + let uiSettings: AnnotationsRequestProcessorsParams['uiSettings']; + + const next = jest.fn((x) => x) as unknown as ReturnType< + ReturnType + >; + + beforeEach(() => { + req = { + body: { + timerange: { + timezone: 'Europe/Minsk', + min: '2022-01-29T22:03:02.317Z', + max: '2022-02-07T09:00:00.000Z', + }, + }, + } as AnnotationsRequestProcessorsParams['req']; + panel = {} as AnnotationsRequestProcessorsParams['panel']; + annotation = { + time_field: 'fooField', + } as AnnotationsRequestProcessorsParams['annotation']; + annotationIndex = { + indexPattern: undefined, + indexPatternString: 'foo*', + }; + capabilities = { + getValidTimeInterval: jest.fn((x) => x), + } as unknown as DefaultSearchCapabilities; + uiSettings = { + get: jest.fn().mockResolvedValue(100), + } as unknown as AnnotationsRequestProcessorsParams['uiSettings']; + }); + + test('should set "size" to 0', async () => { + const doc = await query({ + req, + panel, + annotation, + esQueryConfig, + annotationIndex, + capabilities, + uiSettings, + } as AnnotationsRequestProcessorsParams)(next)({}); + + expect(doc.size).toBe(0); + }); + + test('should apply global query (Lucene)', async () => { + req.body.query = [ + { + query: 'hour_of_day : 1', + language: 'lucene', + }, + ]; + + annotation.ignore_global_filters = 0; + + const doc = await query({ + req, + panel, + annotation, + esQueryConfig, + annotationIndex, + capabilities, + uiSettings, + } as AnnotationsRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [], + "must": Array [ + Object { + "query_string": Object { + "query": "hour_of_day : 1", + }, + }, + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T08:00:00.000Z", + }, + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + test('should apply global query (KQL)', async () => { + req.body.query = [ + { + query: 'hour_of_day : 1', + language: 'kuery', + }, + ]; + + annotation.ignore_global_filters = 0; + + const doc = await query({ + req, + panel, + annotation, + esQueryConfig, + annotationIndex, + capabilities, + uiSettings, + } as AnnotationsRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "hour_of_day": "1", + }, + }, + ], + }, + }, + ], + "must": Array [ + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T08:00:00.000Z", + }, + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + test('should apply global filters', async () => { + req.body.filters = [ + { + meta: { + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + alias: null, + negate: false, + disabled: false, + type: 'exists', + key: 'referer', + value: 'exists', + }, + query: { + exists: { + field: 'referer', + }, + }, + }, + ]; + + annotation.ignore_global_filters = 0; + + const doc = await query({ + req, + panel, + annotation, + esQueryConfig, + annotationIndex, + capabilities, + uiSettings, + } as AnnotationsRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "exists": Object { + "field": "referer", + }, + }, + ], + "must": Array [ + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T08:00:00.000Z", + }, + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + test('should add panel filters and merge it with global one', async () => { + req.body.query = [ + { + query: 'hour_of_day : 1', + language: 'kuery', + }, + ]; + + panel.filter = { + query: 'agent : 2', + language: 'kuery', + }; + + annotation.ignore_global_filters = 0; + annotation.ignore_panel_filters = 0; + + const doc = await query({ + req, + panel, + annotation, + esQueryConfig, + annotationIndex, + capabilities, + uiSettings, + } as AnnotationsRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "hour_of_day": "1", + }, + }, + ], + }, + }, + ], + "must": Array [ + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T08:00:00.000Z", + }, + }, + }, + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "agent": "2", + }, + }, + ], + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + test('should ignore global and panel filters/queries ', async () => { + req.body.query = [ + { + query: 'hour_of_day : 1', + language: 'kuery', + }, + ]; + + req.body.filters = [ + { + meta: { + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + alias: null, + negate: false, + disabled: false, + type: 'exists', + key: 'referer', + value: 'exists', + }, + query: { + exists: { + field: 'referer', + }, + }, + }, + ]; + + panel.filter = { + query: 'agent : 2', + language: 'kuery', + }; + + annotation.ignore_global_filters = 1; + annotation.ignore_panel_filters = 1; + + const doc = await query({ + req, + panel, + annotation, + esQueryConfig, + annotationIndex, + capabilities, + uiSettings, + } as AnnotationsRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [], + "must": Array [ + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T08:00:00.000Z", + }, + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + test('should add annotation query ', async () => { + annotation.query_string = { + query: 'hour_of_day : 1', + language: 'kuery', + }; + + const doc = await query({ + req, + panel, + annotation, + esQueryConfig, + annotationIndex, + capabilities, + uiSettings, + } as AnnotationsRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [], + "must": Array [ + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T08:00:00.000Z", + }, + }, + }, + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "hour_of_day": "1", + }, + }, + ], + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); +}); diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/query.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/query.ts index 1400702a47fd5..eaf2c5ae2e7bf 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/query.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/annotations/query.ts @@ -34,14 +34,12 @@ export const query: AnnotationsRequestProcessorsFunction = ({ const { bucketSize } = getBucketSize(req, 'auto', capabilities, barTargetUiSettings); const { from, to } = getTimerange(req); - doc.size = 0; const queries = !annotation.ignore_global_filters ? req.body.query : []; const filters = !annotation.ignore_global_filters ? req.body.filters : []; + const esQuery = buildEsQuery(indexPattern, queries, filters, esQueryConfig); - doc.query = buildEsQuery(indexPattern, queries, filters, esQueryConfig); - - const boolFilters: unknown[] = [ - { + if (timeField) { + esQuery.bool.must.push({ range: { [timeField]: { gte: from.toISOString(), @@ -49,25 +47,28 @@ export const query: AnnotationsRequestProcessorsFunction = ({ format: 'strict_date_optional_time', }, }, - }, - ]; + }); + } if (annotation.query_string) { - boolFilters.push(buildEsQuery(indexPattern, [annotation.query_string], [], esQueryConfig)); + esQuery.bool.must.push( + buildEsQuery(indexPattern, [annotation.query_string], [], esQueryConfig) + ); } if (!annotation.ignore_panel_filters && panel.filter) { - boolFilters.push(buildEsQuery(indexPattern, [panel.filter], [], esQueryConfig)); + esQuery.bool.must.push(buildEsQuery(indexPattern, [panel.filter], [], esQueryConfig)); } if (annotation.fields) { const fields = annotation.fields.split(/[,\s]+/) || []; fields.forEach((field) => { - boolFilters.push({ exists: { field } }); + esQuery.bool.must.push({ exists: { field } }); }); } - overwrite(doc, 'query.bool.must', boolFilters); + overwrite(doc, 'size', 0); + overwrite(doc, 'query', esQuery); return next(doc); }; diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/query.test.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/query.test.ts new file mode 100644 index 0000000000000..013a5f0314d2d --- /dev/null +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/query.test.ts @@ -0,0 +1,340 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { query } from './query'; + +import type { TableRequestProcessorsFunction, TableRequestProcessorsParams } from './types'; + +describe('query', () => { + let req: TableRequestProcessorsParams['req']; + let panel: TableRequestProcessorsParams['panel']; + let seriesIndex: TableRequestProcessorsParams['seriesIndex']; + let buildSeriesMetaParams: TableRequestProcessorsParams['buildSeriesMetaParams']; + + const next = jest.fn((x) => x) as unknown as ReturnType< + ReturnType + >; + + beforeEach(() => { + req = { + body: { + timerange: { + timezone: 'Europe/Minsk', + min: '2022-01-29T22:03:02.317Z', + max: '2022-02-07T09:00:00.000Z', + }, + }, + } as TableRequestProcessorsParams['req']; + panel = {} as TableRequestProcessorsParams['panel']; + seriesIndex = { + indexPattern: undefined, + indexPatternString: 'foo*', + }; + buildSeriesMetaParams = jest.fn().mockResolvedValue({ timeField: 'fooField' }); + }); + + test('should set "size" to 0', async () => { + const doc = await query({ + req, + panel, + seriesIndex, + buildSeriesMetaParams, + } as TableRequestProcessorsParams)(next)({}); + + expect(doc.size).toBe(0); + }); + + test('should apply global query (Lucene)', async () => { + req.body.query = [ + { + query: 'hour_of_day : 1', + language: 'lucene', + }, + ]; + + panel.ignore_global_filter = 0; + + const doc = await query({ + req, + panel, + seriesIndex, + buildSeriesMetaParams, + } as TableRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [], + "must": Array [ + Object { + "query_string": Object { + "query": "hour_of_day : 1", + }, + }, + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T09:00:00.000Z", + }, + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + test('should apply global query (KQL)', async () => { + req.body.query = [ + { + query: 'hour_of_day : 1', + language: 'kuery', + }, + ]; + + panel.ignore_global_filter = 0; + + const doc = await query({ + req, + panel, + seriesIndex, + buildSeriesMetaParams, + } as TableRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "hour_of_day": "1", + }, + }, + ], + }, + }, + ], + "must": Array [ + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T09:00:00.000Z", + }, + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + test('should apply global filters', async () => { + req.body.filters = [ + { + meta: { + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + alias: null, + negate: false, + disabled: false, + type: 'exists', + key: 'referer', + value: 'exists', + }, + query: { + exists: { + field: 'referer', + }, + }, + }, + ]; + + panel.ignore_global_filter = 0; + + const doc = await query({ + req, + panel, + seriesIndex, + buildSeriesMetaParams, + } as TableRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "exists": Object { + "field": "referer", + }, + }, + ], + "must": Array [ + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T09:00:00.000Z", + }, + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + test('should add panel filters and merge it with global one', async () => { + req.body.query = [ + { + query: 'hour_of_day : 1', + language: 'kuery', + }, + ]; + + panel.filter = { + query: 'agent : 2', + language: 'kuery', + }; + + panel.ignore_global_filter = 0; + + const doc = await query({ + req, + panel, + seriesIndex, + buildSeriesMetaParams, + } as TableRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "hour_of_day": "1", + }, + }, + ], + }, + }, + ], + "must": Array [ + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T09:00:00.000Z", + }, + }, + }, + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "agent": "2", + }, + }, + ], + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + test('should ignore global filters/queries in case is panel.ignore_global_filter = 1 ', async () => { + req.body.query = [ + { + query: 'hour_of_day : 1', + language: 'kuery', + }, + ]; + + req.body.filters = [ + { + meta: { + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + alias: null, + negate: false, + disabled: false, + type: 'exists', + key: 'referer', + value: 'exists', + }, + query: { + exists: { + field: 'referer', + }, + }, + }, + ]; + + panel.ignore_global_filter = 1; + + const doc = await query({ + req, + panel, + seriesIndex, + buildSeriesMetaParams, + } as TableRequestProcessorsParams)(next)({}); + + expect(doc.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [], + "must": Array [ + Object { + "range": Object { + "fooField": Object { + "format": "strict_date_optional_time", + "gte": "2022-01-29T22:03:02.317Z", + "lte": "2022-02-07T09:00:00.000Z", + }, + }, + }, + ], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); +}); diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/query.ts b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/query.ts index d92aa1317b971..1036a4f8a105c 100644 --- a/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/query.ts +++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/query.ts @@ -17,14 +17,10 @@ export const query: TableRequestProcessorsFunction = const { timeField } = await buildSeriesMetaParams(); const { from, to } = getTimerange(req); const indexPattern = seriesIndex.indexPattern || undefined; - - doc.size = 0; - const queries = !panel.ignore_global_filter ? req.body.query : []; const filters = !panel.ignore_global_filter ? req.body.filters : []; - doc.query = buildEsQuery(indexPattern, queries, filters, esQueryConfig); - const boolFilters: unknown[] = []; + const esQuery = buildEsQuery(indexPattern, queries, filters, esQueryConfig); if (timeField) { const timerange = { @@ -37,13 +33,14 @@ export const query: TableRequestProcessorsFunction = }, }; - boolFilters.push(timerange); + esQuery.bool.must.push(timerange); } if (panel.filter) { - boolFilters.push(buildEsQuery(indexPattern, [panel.filter], [], esQueryConfig)); + esQuery.bool.must.push(buildEsQuery(indexPattern, [panel.filter], [], esQueryConfig)); } - overwrite(doc, 'query.bool.must', boolFilters); + overwrite(doc, 'size', 0); + overwrite(doc, 'query', esQuery); return next(doc); }; From ee9f01e925eb6572e84d9845ea52e12ea43679bf Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 8 Feb 2022 15:12:25 +0100 Subject: [PATCH 23/44] Unskip test scripted fields preview (#124358) * unskip test and change we how assert for expected states * REVERT - added .only for flaky test runner * remove .only Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../management/_scripted_fields_preview.js | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/test/functional/apps/management/_scripted_fields_preview.js b/test/functional/apps/management/_scripted_fields_preview.js index a442b521d5d98..b6c941fe21d0a 100644 --- a/test/functional/apps/management/_scripted_fields_preview.js +++ b/test/functional/apps/management/_scripted_fields_preview.js @@ -13,8 +13,15 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['settings']); const SCRIPTED_FIELD_NAME = 'myScriptedField'; - // FLAKY: https://github.com/elastic/kibana/issues/118981 - describe.skip('scripted fields preview', () => { + const scriptResultToJson = (scriptResult) => { + try { + return JSON.parse(scriptResult); + } catch (e) { + expect().fail(`Could JSON.parse script result: "${scriptResult}"`); + } + }; + + describe('scripted fields preview', () => { before(async function () { await browser.setWindowSize(1200, 800); await PageObjects.settings.navigateTo(); @@ -46,7 +53,15 @@ export default function ({ getService, getPageObjects }) { const scriptResults = await PageObjects.settings.executeScriptedField( `doc['bytes'].value * 2` ); - expect(scriptResults.replace(/\s/g, '')).to.contain('"myScriptedField":[6196'); + const [ + { + _id, + [SCRIPTED_FIELD_NAME]: { [0]: scriptedField }, + }, + ] = scriptResultToJson(scriptResults); + expect(_id).to.be.a('string'); + expect(scriptedField).to.be.a('number'); + expect(scriptedField).to.match(/[0-9]+/); }); it('should display additional fields', async function () { @@ -54,7 +69,9 @@ export default function ({ getService, getPageObjects }) { `doc['bytes'].value * 2`, ['bytes'] ); - expect(scriptResults.replace(/\s/g, '')).to.contain('"bytes":3098'); + const [{ _id, bytes }] = scriptResultToJson(scriptResults); + expect(_id).to.be.a('string'); + expect(bytes).to.be.a('number'); }); }); } From df6d386d5087a250ac5eec31edddb830c16c4d21 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 8 Feb 2022 09:14:01 -0500 Subject: [PATCH 24/44] [Fleet] Fix docker registry timeout in integration tests (#124889) --- .../server/integration_tests/docker_registry_helper.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/fleet/server/integration_tests/docker_registry_helper.ts b/x-pack/plugins/fleet/server/integration_tests/docker_registry_helper.ts index bb34dc3258d05..902be3aa35bcd 100644 --- a/x-pack/plugins/fleet/server/integration_tests/docker_registry_helper.ts +++ b/x-pack/plugins/fleet/server/integration_tests/docker_registry_helper.ts @@ -12,6 +12,8 @@ import fetch from 'node-fetch'; const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +const DOCKER_START_TIMEOUT = 5 * 60 * 1000; // 5 minutes + export function useDockerRegistry() { const packageRegistryPort = process.env.FLEET_PACKAGE_REGISTRY_PORT || '8081'; @@ -32,8 +34,9 @@ export function useDockerRegistry() { isExited = true; }); - let retries = 0; - while (!isExited && retries++ <= 20) { + const startedAt = Date.now(); + + while (!isExited && Date.now() - startedAt <= DOCKER_START_TIMEOUT) { try { const res = await fetch(`http://localhost:${packageRegistryPort}/`); if (res.status === 200) { From b8abe763c9b13b805baf157465489aac5060590c Mon Sep 17 00:00:00 2001 From: Lee Drengenberg Date: Tue, 8 Feb 2022 08:44:50 -0600 Subject: [PATCH 25/44] split date_nested kbn_archive out of es_archive (#124646) * split kbn_archive out of es_archive * add missing unload of kbn_archive Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/functional/apps/discover/_date_nested.ts | 5 +++++ .../apps/discover/_saved_queries.ts | 4 ++++ .../apps/discover/_search_on_page_load.ts | 6 +++++- .../es_archiver/date_nested/data.json | 21 ++----------------- .../es_archiver/date_nested/mappings.json | 9 +++++++- .../fixtures/kbn_archiver/date_nested.json | 15 +++++++++++++ 6 files changed, 39 insertions(+), 21 deletions(-) create mode 100644 test/functional/fixtures/kbn_archiver/date_nested.json diff --git a/test/functional/apps/discover/_date_nested.ts b/test/functional/apps/discover/_date_nested.ts index 8297d84832ff6..83b9bdd44a5be 100644 --- a/test/functional/apps/discover/_date_nested.ts +++ b/test/functional/apps/discover/_date_nested.ts @@ -13,10 +13,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); const security = getService('security'); + const kibanaServer = getService('kibanaServer'); describe('timefield is a date in a nested field', function () { before(async function () { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/date_nested'); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/date_nested.json' + ); await security.testUser.setRoles(['kibana_admin', 'kibana_date_nested']); await PageObjects.common.navigateToApp('discover'); }); @@ -24,6 +28,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async function unloadMakelogs() { await security.testUser.restoreDefaults(); await esArchiver.unload('test/functional/fixtures/es_archiver/date_nested'); + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/date_nested'); }); it('should show an error message', async function () { diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts index b7d19807e563e..5a8b14545508c 100644 --- a/test/functional/apps/discover/_saved_queries.ts +++ b/test/functional/apps/discover/_saved_queries.ts @@ -42,6 +42,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/date_nested.json' + ); await esArchiver.load('test/functional/fixtures/es_archiver/date_nested'); await esArchiver.load('test/functional/fixtures/es_archiver/logstash_functional'); @@ -53,6 +56,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/date_nested'); await esArchiver.unload('test/functional/fixtures/es_archiver/date_nested'); await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); await PageObjects.common.unsetTime(); diff --git a/test/functional/apps/discover/_search_on_page_load.ts b/test/functional/apps/discover/_search_on_page_load.ts index 277d2e72d729f..0198881e981b8 100644 --- a/test/functional/apps/discover/_search_on_page_load.ts +++ b/test/functional/apps/discover/_search_on_page_load.ts @@ -43,6 +43,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // and load a set of data await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await esArchiver.load('test/functional/fixtures/es_archiver/date_nested'); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/date_nested.json' + ); await kibanaServer.uiSettings.replace(defaultSettings); await PageObjects.common.navigateToApp('discover'); @@ -50,7 +53,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); - await esArchiver.load('test/functional/fixtures/es_archiver/date_nested'); + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/date_nested'); + await esArchiver.unload('test/functional/fixtures/es_archiver/date_nested'); await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); }); diff --git a/test/functional/fixtures/es_archiver/date_nested/data.json b/test/functional/fixtures/es_archiver/date_nested/data.json index bb623f93627c7..e2ffb210d1208 100644 --- a/test/functional/fixtures/es_archiver/date_nested/data.json +++ b/test/functional/fixtures/es_archiver/date_nested/data.json @@ -1,30 +1,13 @@ -{ - "type": "doc", - "value": { - "id": "index-pattern:date-nested", - "index": ".kibana", - "source": { - "index-pattern": { - "fields":"[]", - "timeFieldName": "nested.timestamp", - "title": "date-nested" - }, - "type": "index-pattern" - } - } -} - - { "type": "doc", "value": { "id": "date-nested-1", "index": "date-nested", "source": { - "message" : "test", + "message": "test", "nested": { "timestamp": "2021-06-30T12:00:00.123Z" } } } -} +} \ No newline at end of file diff --git a/test/functional/fixtures/es_archiver/date_nested/mappings.json b/test/functional/fixtures/es_archiver/date_nested/mappings.json index f30e5863f4f8b..b3f995cae173d 100644 --- a/test/functional/fixtures/es_archiver/date_nested/mappings.json +++ b/test/functional/fixtures/es_archiver/date_nested/mappings.json @@ -1,6 +1,8 @@ { "type": "index", "value": { + "aliases": { + }, "index": "date-nested", "mappings": { "properties": { @@ -8,6 +10,11 @@ "type": "text" }, "nested": { + "properties": { + "timestamp": { + "type": "date" + } + }, "type": "nested" } } @@ -19,4 +26,4 @@ } } } -} +} \ No newline at end of file diff --git a/test/functional/fixtures/kbn_archiver/date_nested.json b/test/functional/fixtures/kbn_archiver/date_nested.json new file mode 100644 index 0000000000000..c015a5b0bbd62 --- /dev/null +++ b/test/functional/fixtures/kbn_archiver/date_nested.json @@ -0,0 +1,15 @@ +{ + "attributes": { + "fields": "[]", + "timeFieldName": "nested.timestamp", + "title": "date-nested" + }, + "coreMigrationVersion": "8.2.0", + "id": "date-nested", + "migrationVersion": { + "index-pattern": "8.0.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzIyLDFd" +} \ No newline at end of file From a88756adb6e508b22f033faca29cca335eccf080 Mon Sep 17 00:00:00 2001 From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> Date: Tue, 8 Feb 2022 15:52:33 +0100 Subject: [PATCH 26/44] [Fleet] Create package policy tests (#124697) * unit tests for create package policy submit * unit tests for create package policy submit * added tests for vars * added tests for navigate * tests for step_define_package_policy * added tests for step_configure_package * extracted duplicated max package name fn to common * added tests for step_select_hosts --- x-pack/plugins/fleet/common/services/index.ts | 1 + .../common/services/max_package_name.test.ts | 39 ++ .../fleet/common/services/max_package_name.ts | 20 + .../create_package_policy_page/index.test.tsx | 454 +++++++++++++++++- .../step_configure_package.test.tsx | 153 ++++++ .../step_define_package_policy.test.tsx | 202 ++++++++ .../step_define_package_policy.tsx | 17 +- .../step_select_hosts.test.tsx | 157 ++++++ .../fleet/server/services/package_policy.ts | 13 +- 9 files changed, 1025 insertions(+), 31 deletions(-) create mode 100644 x-pack/plugins/fleet/common/services/max_package_name.test.ts create mode 100644 x-pack/plugins/fleet/common/services/max_package_name.ts create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.test.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.test.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_hosts.test.tsx diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts index 7698308270fff..472047f6f496e 100644 --- a/x-pack/plugins/fleet/common/services/index.ts +++ b/x-pack/plugins/fleet/common/services/index.ts @@ -35,3 +35,4 @@ export { export { normalizeHostsForAgents } from './hosts_utils'; export { splitPkgKey } from './split_pkg_key'; +export { getMaxPackageName } from './max_package_name'; diff --git a/x-pack/plugins/fleet/common/services/max_package_name.test.ts b/x-pack/plugins/fleet/common/services/max_package_name.test.ts new file mode 100644 index 0000000000000..f2631257d7e3f --- /dev/null +++ b/x-pack/plugins/fleet/common/services/max_package_name.test.ts @@ -0,0 +1,39 @@ +/* + * 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 { getMaxPackageName } from '.'; + +describe('get max package policy name', () => { + it('should return index 1 when no policies', () => { + const name = getMaxPackageName('apache', []); + expect(name).toEqual('apache-1'); + }); + + it('should return index 1 when policies with other name', () => { + const name = getMaxPackageName('apache', [{ name: 'package' } as any]); + expect(name).toEqual('apache-1'); + }); + + it('should return index 2 when policies 1 exists', () => { + const name = getMaxPackageName('apache', [{ name: 'apache-1' } as any]); + expect(name).toEqual('apache-2'); + }); + + it('should return index 11 when policy 10 is max', () => { + const name = getMaxPackageName('apache', [ + { name: 'apache-10' } as any, + { name: 'apache-9' } as any, + { name: 'package' } as any, + ]); + expect(name).toEqual('apache-11'); + }); + + it('should return index 1 when policies undefined', () => { + const name = getMaxPackageName('apache'); + expect(name).toEqual('apache-1'); + }); +}); diff --git a/x-pack/plugins/fleet/common/services/max_package_name.ts b/x-pack/plugins/fleet/common/services/max_package_name.ts new file mode 100644 index 0000000000000..6078d4e6b7bbf --- /dev/null +++ b/x-pack/plugins/fleet/common/services/max_package_name.ts @@ -0,0 +1,20 @@ +/* + * 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 function getMaxPackageName(packageName: string, packagePolicies?: Array<{ name: string }>) { + // Retrieve highest number appended to package policy name and increment it by one + const pkgPoliciesNamePattern = new RegExp(`${packageName}-(\\d+)`); + + const maxPkgPolicyName = Math.max( + ...(packagePolicies ?? []) + .filter((ds) => Boolean(ds.name.match(pkgPoliciesNamePattern))) + .map((ds) => parseInt(ds.name.match(pkgPoliciesNamePattern)![1], 10)), + 0 + ); + + return `${packageName}-${maxPkgPolicyName + 1}`; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.test.tsx index b8bae0cb1f541..7a15276afbcd1 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.test.tsx @@ -5,19 +5,152 @@ * 2.0. */ -import { Route } from 'react-router-dom'; +import { Route, useLocation, useHistory } from 'react-router-dom'; import React from 'react'; -import { act } from 'react-test-renderer'; +import { fireEvent, act, waitFor } from '@testing-library/react'; import type { MockedFleetStartServices, TestRenderer } from '../../../../../mock'; import { createFleetTestRendererMock } from '../../../../../mock'; import { FLEET_ROUTING_PATHS, pagePathGetters, PLUGIN_ID } from '../../../constants'; import type { CreatePackagePolicyRouteState } from '../../../types'; +import { + sendCreatePackagePolicy, + sendCreateAgentPolicy, + sendGetAgentStatus, + useIntraAppState, + useStartServices, +} from '../../../hooks'; + import { CreatePackagePolicyPage } from './index'; +jest.mock('../../../hooks', () => { + return { + ...jest.requireActual('../../../hooks'), + useFleetStatus: jest.fn().mockReturnValue({ isReady: true } as any), + sendGetStatus: jest + .fn() + .mockResolvedValue({ data: { isReady: true, missing_requirements: [] } }), + sendGetAgentStatus: jest.fn().mockResolvedValue({ data: { results: { total: 0 } } }), + useGetAgentPolicies: jest.fn().mockReturnValue({ + data: { + items: [{ id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' }], + }, + error: undefined, + isLoading: false, + resendRequest: jest.fn(), + } as any), + sendGetOneAgentPolicy: jest.fn().mockResolvedValue({ + data: { item: { id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' } }, + }), + useGetPackageInfoByKey: jest.fn().mockReturnValue({ + data: { + item: { + name: 'nginx', + title: 'Nginx', + version: '1.3.0', + release: 'ga', + description: 'Collect logs and metrics from Nginx HTTP servers with Elastic Agent.', + policy_templates: [ + { + name: 'nginx', + title: 'Nginx logs and metrics', + description: 'Collect logs and metrics from Nginx instances', + inputs: [ + { + type: 'logfile', + title: 'Collect logs from Nginx instances', + description: 'Collecting Nginx access and error logs', + }, + ], + multiple: true, + }, + ], + data_streams: [ + { + type: 'logs', + dataset: 'nginx.access', + title: 'Nginx access logs', + release: 'experimental', + ingest_pipeline: 'default', + streams: [ + { + input: 'logfile', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Paths', + multi: true, + required: true, + show_user: true, + default: ['/var/log/nginx/access.log*'], + }, + ], + template_path: 'stream.yml.hbs', + title: 'Nginx access logs', + description: 'Collect Nginx access logs', + enabled: true, + }, + ], + package: 'nginx', + path: 'access', + }, + ], + latestVersion: '1.3.0', + removable: true, + keepPoliciesUpToDate: false, + status: 'not_installed', + }, + }, + isLoading: false, + }), + sendCreatePackagePolicy: jest + .fn() + .mockResolvedValue({ data: { item: { id: 'policy-1', inputs: [] } } }), + sendCreateAgentPolicy: jest.fn().mockResolvedValue({ + data: { item: { id: 'agent-policy-2', name: 'Agent policy 2', namespace: 'default' } }, + }), + useIntraAppState: jest.fn().mockReturnValue({}), + useStartServices: jest.fn().mockReturnValue({ + application: { navigateToApp: jest.fn() }, + notifications: { + toasts: { + addError: jest.fn(), + addSuccess: jest.fn(), + }, + }, + docLinks: { + links: { + fleet: {}, + }, + }, + http: { + basePath: { + get: () => 'http://localhost:5620', + prepend: (url: string) => 'http://localhost:5620' + url, + }, + }, + chrome: { + docTitle: { + change: jest.fn(), + }, + setBreadcrumbs: jest.fn(), + }, + }), + }; +}); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: jest.fn().mockReturnValue({ search: '' }), + useHistory: jest.fn().mockReturnValue({ + push: jest.fn(), + }), +})); + describe('when on the package policy create page', () => { - const createPageUrlPath = pagePathGetters.add_integration_to_policy({ pkgkey: 'nginx-0.3.7' })[1]; + const createPageUrlPath = pagePathGetters.add_integration_to_policy({ pkgkey: 'nginx-1.3.0' })[1]; let testRenderer: TestRenderer; let renderResult: ReturnType; @@ -47,6 +180,8 @@ describe('when on the package policy create page', () => { pathname: createPageUrlPath, state: expectedRouteState, }); + + (useIntraAppState as jest.MockedFunction).mockReturnValue(expectedRouteState); }); describe('and the cancel Link or Button is clicked', () => { @@ -67,16 +202,325 @@ describe('when on the package policy create page', () => { }); }); - it('should use custom "cancel" URL', () => { + test('should use custom "cancel" URL', () => { expect(cancelLink.href).toBe(expectedRouteState.onCancelUrl); expect(cancelButton.href).toBe(expectedRouteState.onCancelUrl); }); }); }); + + describe('submit page', () => { + const newPackagePolicy = { + description: '', + enabled: true, + inputs: [ + { + enabled: true, + policy_template: 'nginx', + streams: [ + { + data_stream: { + dataset: 'nginx.access', + type: 'logs', + }, + enabled: true, + vars: { + paths: { + type: 'text', + value: ['/var/log/nginx/access.log*'], + }, + }, + }, + ], + type: 'logfile', + }, + ], + name: 'nginx-1', + namespace: 'default', + output_id: '', + package: { + name: 'nginx', + title: 'Nginx', + version: '1.3.0', + }, + policy_id: 'agent-policy-1', + vars: undefined, + }; + + test('should create package policy on submit when query param agent policy id is set', async () => { + (useLocation as jest.MockedFunction).mockImplementationOnce(() => ({ + search: 'policyId=agent-policy-1', + })); + + render(); + + let saveBtn: HTMLElement; + + await waitFor(() => { + saveBtn = renderResult.getByText(/Save and continue/).closest('button')!; + expect(saveBtn).not.toBeDisabled(); + }); + + await act(async () => { + fireEvent.click(saveBtn); + }); + + expect(sendCreatePackagePolicy as jest.MockedFunction).toHaveBeenCalledWith({ + ...newPackagePolicy, + policy_id: 'agent-policy-1', + }); + expect(sendCreateAgentPolicy as jest.MockedFunction).not.toHaveBeenCalled(); + + await waitFor(() => { + expect(renderResult.getByText('Nginx integration added')).toBeInTheDocument(); + }); + }); + + describe('on save navigate', () => { + async function setupSaveNavigate(routeState: any) { + (useIntraAppState as jest.MockedFunction).mockReturnValue(routeState); + render(); + + await act(async () => { + fireEvent.click(renderResult.getByText('Existing hosts')!); + }); + + await act(async () => { + fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!); + }); + + await act(async () => { + fireEvent.click( + renderResult.getByText(/Add Elastic Agent to your hosts/).closest('button')! + ); + }); + } + + test('should navigate to save navigate path if set', async () => { + const routeState = { + onSaveNavigateTo: [PLUGIN_ID, { path: '/save/url/here' }], + }; + + await setupSaveNavigate(routeState); + + expect(useStartServices().application.navigateToApp).toHaveBeenCalledWith(PLUGIN_ID, { + path: '/save/url/here', + }); + }); + + test('should navigate to save navigate path with query param if set', async () => { + const mockUseLocation = useLocation as jest.MockedFunction; + mockUseLocation.mockReturnValue({ + search: 'policyId=agent-policy-1', + }); + + const routeState = { + onSaveNavigateTo: [PLUGIN_ID, { path: '/save/url/here' }], + }; + + await setupSaveNavigate(routeState); + + expect(useStartServices().application.navigateToApp).toHaveBeenCalledWith(PLUGIN_ID, { + path: '/policies/agent-policy-1', + }); + + mockUseLocation.mockReturnValue({ + search: '', + }); + }); + + test('should navigate to save navigate app if set', async () => { + const routeState = { + onSaveNavigateTo: [PLUGIN_ID], + }; + await setupSaveNavigate(routeState); + + expect(useStartServices().application.navigateToApp).toHaveBeenCalledWith(PLUGIN_ID); + }); + + test('should set history if no routeState', async () => { + await setupSaveNavigate({}); + + expect(useHistory().push).toHaveBeenCalledWith('/policies/agent-policy-1'); + }); + }); + + describe('without query param', () => { + beforeEach(() => { + render(); + + (sendCreateAgentPolicy as jest.MockedFunction).mockClear(); + (sendCreatePackagePolicy as jest.MockedFunction).mockClear(); + }); + + test('should create agent policy before creating package policy on submit when new hosts is selected', async () => { + await waitFor(() => { + renderResult.getByDisplayValue('Agent policy 2'); + }); + + await act(async () => { + fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!); + }); + + expect(sendCreateAgentPolicy as jest.MockedFunction).toHaveBeenCalledWith( + { + description: '', + monitoring_enabled: ['logs', 'metrics'], + name: 'Agent policy 2', + namespace: 'default', + }, + { withSysMonitoring: true } + ); + expect(sendCreatePackagePolicy as jest.MockedFunction).toHaveBeenCalledWith({ + ...newPackagePolicy, + policy_id: 'agent-policy-2', + }); + + await waitFor(() => { + expect(renderResult.getByText('Nginx integration added')).toBeInTheDocument(); + }); + }); + + test('should disable submit button on invalid form with empty agent policy name', async () => { + await act(async () => { + fireEvent.change(renderResult.getByLabelText('New agent policy name'), { + target: { value: '' }, + }); + }); + + renderResult.getByText( + 'Your integration policy has errors. Please fix them before saving.' + ); + expect(renderResult.getByText(/Save and continue/).closest('button')!).toBeDisabled(); + }); + + test('should not show modal if agent policy has agents', async () => { + (sendGetAgentStatus as jest.MockedFunction).mockResolvedValueOnce({ + data: { results: { total: 1 } }, + }); + + await act(async () => { + fireEvent.click(renderResult.getByText('Existing hosts')!); + }); + + await act(async () => { + fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!); + }); + + await waitFor(() => { + expect(renderResult.getByText('This action will update 1 agent')).toBeInTheDocument(); + }); + + await act(async () => { + fireEvent.click( + renderResult.getAllByText(/Save and deploy changes/)[1].closest('button')! + ); + }); + + expect(sendCreatePackagePolicy as jest.MockedFunction).toHaveBeenCalled(); + }); + + describe('create package policy with existing agent policy', () => { + beforeEach(async () => { + await act(async () => { + fireEvent.click(renderResult.getByText('Existing hosts')!); + }); + }); + + test('should creating package policy with existing host', async () => { + await act(async () => { + fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!); + }); + + expect(sendCreateAgentPolicy as jest.MockedFunction).not.toHaveBeenCalled(); + expect(sendCreatePackagePolicy as jest.MockedFunction).toHaveBeenCalledWith({ + ...newPackagePolicy, + policy_id: 'agent-policy-1', + }); + + await waitFor(() => { + expect(renderResult.getByText('Nginx integration added')).toBeInTheDocument(); + }); + }); + + test('should disable submit button on invalid form with empty name', async () => { + await act(async () => { + fireEvent.change(renderResult.getByLabelText('Integration name'), { + target: { value: '' }, + }); + }); + + renderResult.getByText( + 'Your integration policy has errors. Please fix them before saving.' + ); + expect(renderResult.getByText(/Save and continue/).closest('button')!).toBeDisabled(); + }); + + test('should disable submit button on invalid form with empty package var', async () => { + await act(async () => { + fireEvent.click(renderResult.getByLabelText('Show logfile inputs')); + }); + + await act(async () => { + fireEvent.change(renderResult.getByDisplayValue('/var/log/nginx/access.log*'), { + target: { value: '' }, + }); + }); + + renderResult.getByText( + 'Your integration policy has errors. Please fix them before saving.' + ); + expect(renderResult.getByText(/Save and continue/).closest('button')!).toBeDisabled(); + }); + + test('should submit form with changed package var', async () => { + await act(async () => { + fireEvent.click(renderResult.getByLabelText('Show logfile inputs')); + }); + + await act(async () => { + fireEvent.change(renderResult.getByDisplayValue('/var/log/nginx/access.log*'), { + target: { value: '/path/to/log' }, + }); + }); + + await act(async () => { + fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!); + }); + + expect(sendCreatePackagePolicy as jest.MockedFunction).toHaveBeenCalledWith({ + ...newPackagePolicy, + inputs: [ + { + ...newPackagePolicy.inputs[0], + streams: [ + { + ...newPackagePolicy.inputs[0].streams[0], + vars: { + paths: { + type: 'text', + value: ['/path/to/log'], + }, + }, + }, + ], + }, + ], + }); + }); + }); + }); + }); }); const mockApiCalls = (http: MockedFleetStartServices['http']) => { - http.get.mockImplementation(async (path) => { + http.get.mockImplementation(async (path: any) => { + if (path === '/api/fleet/agents/setup') { + return Promise.resolve({ data: { results: { total: 0 } } }); + } + if (path === '/api/fleet/package_policies') { + return Promise.resolve({ data: { items: [] } }); + } const err = new Error(`API [GET ${path}] is not MOCKED!`); // eslint-disable-next-line no-console console.log(err); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.test.tsx new file mode 100644 index 0000000000000..543747307908e --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.test.tsx @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { act, fireEvent, waitFor } from '@testing-library/react'; + +import type { TestRenderer } from '../../../../../mock'; +import { createFleetTestRendererMock } from '../../../../../mock'; +import type { NewPackagePolicy, PackageInfo } from '../../../types'; + +import { StepConfigurePackagePolicy } from './step_configure_package'; + +describe('StepConfigurePackage', () => { + let packageInfo: PackageInfo; + let packagePolicy: NewPackagePolicy; + const mockUpdatePackagePolicy = jest.fn().mockImplementation((val: any) => { + packagePolicy = { + ...val, + ...packagePolicy, + }; + }); + + const validationResults = { name: null, description: null, namespace: null, inputs: {} }; + + let testRenderer: TestRenderer; + let renderResult: ReturnType; + const render = () => + (renderResult = testRenderer.render( + + )); + + beforeEach(() => { + packageInfo = { + name: 'nginx', + title: 'Nginx', + version: '1.3.0', + release: 'ga', + description: 'Collect logs and metrics from Nginx HTTP servers with Elastic Agent.', + format_version: '', + owner: { github: '' }, + assets: {} as any, + policy_templates: [ + { + name: 'nginx', + title: 'Nginx logs and metrics', + description: 'Collect logs and metrics from Nginx instances', + inputs: [ + { + type: 'logfile', + title: 'Collect logs from Nginx instances', + description: 'Collecting Nginx access and error logs', + }, + ], + multiple: true, + }, + ], + data_streams: [ + { + type: 'logs', + dataset: 'nginx.access', + title: 'Nginx access logs', + release: 'experimental', + ingest_pipeline: 'default', + streams: [ + { + input: 'logfile', + vars: [ + { + name: 'paths', + type: 'text', + title: 'Paths', + multi: true, + required: true, + show_user: true, + default: ['/var/log/nginx/access.log*'], + }, + ], + template_path: 'stream.yml.hbs', + title: 'Nginx access logs', + description: 'Collect Nginx access logs', + enabled: true, + }, + ], + package: 'nginx', + path: 'access', + }, + ], + latestVersion: '1.3.0', + removable: true, + keepPoliciesUpToDate: false, + status: 'not_installed', + }; + packagePolicy = { + name: 'nginx-1', + description: 'desc', + namespace: 'default', + policy_id: '', + enabled: true, + output_id: '', + inputs: [ + { + type: 'logfile', + policy_template: 'nginx', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'nginx.access' }, + vars: { + paths: { value: ['/var/log/nginx/access.log*'], type: 'text' }, + tags: { value: ['nginx-access'], type: 'text' }, + preserve_original_event: { value: false, type: 'bool' }, + processors: { type: 'yaml' }, + }, + }, + ], + }, + ], + }; + testRenderer = createFleetTestRendererMock(); + }); + + it('should show nothing to configure if no matching integration', () => { + packageInfo.policy_templates = []; + render(); + + waitFor(() => { + expect(renderResult.getByText('Nothing to configure')).toBeInTheDocument(); + }); + }); + + it('should show inputs of policy templates and update package policy with input enabled: false', async () => { + render(); + + waitFor(() => { + expect(renderResult.getByText('Collect logs from Nginx instances')).toBeInTheDocument(); + }); + act(() => { + fireEvent.click(renderResult.getByRole('switch')); + }); + expect(mockUpdatePackagePolicy.mock.calls[0][0].inputs[0].enabled).toEqual(false); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.test.tsx new file mode 100644 index 0000000000000..a15692b718a32 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.test.tsx @@ -0,0 +1,202 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { act, fireEvent, waitFor } from '@testing-library/react'; + +import type { TestRenderer } from '../../../../../mock'; +import { createFleetTestRendererMock } from '../../../../../mock'; +import type { AgentPolicy, NewPackagePolicy, PackageInfo } from '../../../types'; + +import { useGetPackagePolicies } from '../../../hooks'; + +import { StepDefinePackagePolicy } from './step_define_package_policy'; + +jest.mock('../../../hooks', () => { + return { + ...jest.requireActual('../../../hooks'), + useGetPackagePolicies: jest.fn().mockReturnValue({ + data: { + items: [{ name: 'nginx-1' }, { name: 'other-policy' }], + }, + isLoading: false, + }), + useFleetStatus: jest.fn().mockReturnValue({ isReady: true } as any), + sendGetStatus: jest + .fn() + .mockResolvedValue({ data: { isReady: true, missing_requirements: [] } }), + }; +}); + +describe('StepDefinePackagePolicy', () => { + const packageInfo: PackageInfo = { + name: 'apache', + version: '1.0.0', + description: '', + format_version: '', + release: 'ga', + owner: { github: '' }, + title: 'Apache', + latestVersion: '', + assets: {} as any, + status: 'not_installed', + vars: [ + { + show_user: true, + name: 'Show user var', + type: 'string', + default: 'showUserVarVal', + }, + { + required: true, + name: 'Required var', + type: 'bool', + }, + { + name: 'Advanced var', + type: 'bool', + default: true, + }, + ], + }; + const agentPolicy: AgentPolicy = { + id: 'agent-policy-1', + namespace: 'ns', + name: 'Agent policy 1', + is_managed: false, + status: 'active', + updated_at: '', + updated_by: '', + revision: 1, + package_policies: [], + }; + let packagePolicy: NewPackagePolicy; + const mockUpdatePackagePolicy = jest.fn().mockImplementation((val: any) => { + packagePolicy = { + ...val, + ...packagePolicy, + }; + }); + + const validationResults = { name: null, description: null, namespace: null, inputs: {} }; + + let testRenderer: TestRenderer; + let renderResult: ReturnType; + const render = () => + (renderResult = testRenderer.render( + + )); + + beforeEach(() => { + packagePolicy = { + name: '', + description: 'desc', + namespace: 'default', + policy_id: '', + enabled: true, + output_id: '', + inputs: [], + }; + testRenderer = createFleetTestRendererMock(); + }); + + describe('default API response', () => { + beforeEach(() => { + render(); + }); + + it('should set index 1 name to package policy on init if no package policies exist for this package', () => { + waitFor(() => { + expect(renderResult.getByDisplayValue('apache-1')).toBeInTheDocument(); + expect(renderResult.getByDisplayValue('desc')).toBeInTheDocument(); + }); + + expect(mockUpdatePackagePolicy.mock.calls[0]).toEqual([ + { + description: 'desc', + enabled: true, + inputs: [], + name: 'apache-1', + namespace: 'default', + policy_id: 'agent-policy-1', + output_id: '', + package: { + name: 'apache', + title: 'Apache', + version: '1.0.0', + }, + vars: { + 'Advanced var': { + type: 'bool', + value: true, + }, + 'Required var': { + type: 'bool', + value: undefined, + }, + 'Show user var': { + type: 'string', + value: 'showUserVarVal', + }, + }, + }, + ]); + expect(mockUpdatePackagePolicy.mock.calls[1]).toEqual([ + { + namespace: 'ns', + policy_id: 'agent-policy-1', + }, + ]); + }); + + it('should display vars coming from package policy', async () => { + waitFor(() => { + expect(renderResult.getByDisplayValue('showUserVarVal')).toBeInTheDocument(); + expect(renderResult.getByRole('switch')).toHaveAttribute('aria-label', 'Required var'); + expect(renderResult.getByText('Required var is required')).toHaveAttribute( + 'class', + 'euiFormErrorText' + ); + }); + + await act(async () => { + fireEvent.click(renderResult.getByText('Advanced options').closest('button')!); + }); + + waitFor(() => { + expect(renderResult.getByRole('switch')).toHaveAttribute('aria-label', 'Advanced var'); + }); + }); + }); + + it('should set incremented name if other package policies exist', () => { + (useGetPackagePolicies as jest.MockedFunction).mockReturnValueOnce({ + data: { + items: [ + { name: 'apache-1' }, + { name: 'apache-2' }, + { name: 'apache-9' }, + { name: 'apache-10' }, + ], + }, + isLoading: false, + }); + + render(); + + waitFor(() => { + expect(renderResult.getByDisplayValue('apache-11')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx index 5afe87901d1b2..7fcddf4439557 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx @@ -27,7 +27,7 @@ import { packageToPackagePolicy, pkgKeyFromPackageInfo } from '../../../services import { Loading } from '../../../components'; import { useStartServices, useGetPackagePolicies } from '../../../hooks'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; -import { SO_SEARCH_LIMIT } from '../../../../../../common'; +import { SO_SEARCH_LIMIT, getMaxPackageName } from '../../../../../../common'; import { isAdvancedVar } from './services'; import type { PackagePolicyValidationResults } from './services'; @@ -99,20 +99,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ // If package has changed, create shell package policy with input&stream values based on package info if (currentPkgKey !== pkgKey) { - // Retrieve highest number appended to package policy name and increment it by one - const pkgPoliciesNamePattern = new RegExp(`${packageInfo.name}-(\\d+)`); - const pkgPoliciesWithMatchingNames = packagePolicyData?.items - ? packagePolicyData.items - .filter((ds) => Boolean(ds.name.match(pkgPoliciesNamePattern))) - .map((ds) => parseInt(ds.name.match(pkgPoliciesNamePattern)![1], 10)) - .sort((a, b) => a - b) - : []; - - const incrementedName = `${packageInfo.name}-${ - pkgPoliciesWithMatchingNames.length - ? pkgPoliciesWithMatchingNames[pkgPoliciesWithMatchingNames.length - 1] + 1 - : 1 - }`; + const incrementedName = getMaxPackageName(packageInfo.name, packagePolicyData?.items); updatePackagePolicy( packageToPackagePolicy( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_hosts.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_hosts.test.tsx new file mode 100644 index 0000000000000..0c9f450e83dae --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_hosts.test.tsx @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { act, fireEvent, waitFor } from '@testing-library/react'; + +import type { TestRenderer } from '../../../../../mock'; +import { createFleetTestRendererMock } from '../../../../../mock'; + +import { useGetAgentPolicies } from '../../../hooks'; +import type { AgentPolicy, PackageInfo } from '../../../types'; + +import { StepSelectHosts } from './step_select_hosts'; + +jest.mock('../../../hooks', () => { + return { + ...jest.requireActual('../../../hooks'), + useGetAgentPolicies: jest.fn(), + }; +}); + +describe('StepSelectHosts', () => { + const packageInfo: PackageInfo = { + name: 'apache', + version: '1.0.0', + description: '', + format_version: '', + release: 'ga', + owner: { github: '' }, + title: 'Apache', + latestVersion: '', + assets: {} as any, + status: 'not_installed', + vars: [], + }; + const agentPolicy: AgentPolicy = { + id: 'agent-policy-1', + namespace: 'default', + name: 'Agent policy 1', + is_managed: false, + status: 'active', + updated_at: '', + updated_by: '', + revision: 1, + package_policies: [], + }; + const newAgentPolicy = { + name: '', + namespace: 'default', + }; + const validation = {}; + + let testRenderer: TestRenderer; + let renderResult: ReturnType; + const render = () => + (renderResult = testRenderer.render( + + )); + beforeEach(() => { + testRenderer = createFleetTestRendererMock(); + }); + + it('should display create form when no agent policies', () => { + (useGetAgentPolicies as jest.MockedFunction).mockReturnValue({ + data: { + items: [], + }, + }); + + render(); + + waitFor(() => { + expect(renderResult.getByText('Agent policy 1')).toBeInTheDocument(); + }); + expect(renderResult.queryByRole('tablist')).not.toBeInTheDocument(); + }); + + it('should display tabs with New hosts selected when agent policies exist', () => { + (useGetAgentPolicies as jest.MockedFunction).mockReturnValue({ + data: { + items: [{ id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' }], + }, + }); + + render(); + + waitFor(() => { + expect(renderResult.getByRole('tablist')).toBeInTheDocument(); + expect(renderResult.getByText('Agent policy 2')).toBeInTheDocument(); + }); + expect(renderResult.getByText('New hosts').closest('button')).toHaveAttribute( + 'aria-selected', + 'true' + ); + }); + + it('should display dropdown with agent policy selected when Existing hosts selected', () => { + (useGetAgentPolicies as jest.MockedFunction).mockReturnValue({ + data: { + items: [{ id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' }], + }, + }); + + render(); + + waitFor(() => { + expect(renderResult.getByRole('tablist')).toBeInTheDocument(); + }); + act(() => { + fireEvent.click(renderResult.getByText('Existing hosts').closest('button')!); + }); + + expect(renderResult.getAllByRole('option').length).toEqual(1); + expect(renderResult.getByText('Agent policy 1').closest('select')).toBeInTheDocument(); + }); + + it('should display dropdown without preselected value when Existing hosts selected with mulitple agent policies', () => { + (useGetAgentPolicies as jest.MockedFunction).mockReturnValue({ + data: { + items: [ + { id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' }, + { id: 'agent-policy-2', name: 'Agent policy 2', namespace: 'default' }, + ], + }, + }); + + render(); + + waitFor(() => { + expect(renderResult.getByRole('tablist')).toBeInTheDocument(); + }); + act(() => { + fireEvent.click(renderResult.getByText('Existing hosts').closest('button')!); + }); + + expect(renderResult.getAllByRole('option').length).toEqual(2); + waitFor(() => { + expect(renderResult.getByText('An agent policy is required.')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 840642492b262..69d7a81a24efd 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -29,6 +29,7 @@ import { validatePackagePolicy, validationHasErrors, SO_SEARCH_LIMIT, + getMaxPackageName, } from '../../common'; import type { DeletePackagePoliciesResponse, @@ -1398,15 +1399,5 @@ export async function incrementPackageName( kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "${packageName}"`, }); - // Retrieve highest number appended to package policy name and increment it by one - const pkgPoliciesNamePattern = new RegExp(`${packageName}-(\\d+)`); - - const maxPkgPolicyName = Math.max( - ...(packagePolicyData?.items ?? []) - .filter((ds) => Boolean(ds.name.match(pkgPoliciesNamePattern))) - .map((ds) => parseInt(ds.name.match(pkgPoliciesNamePattern)![1], 10)), - 0 - ); - - return `${packageName}-${maxPkgPolicyName + 1}`; + return getMaxPackageName(packageName, packagePolicyData?.items); } From 535acb782ce0b5d6f5ba10c76880e748d76e884e Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Tue, 8 Feb 2022 08:22:46 -0700 Subject: [PATCH 27/44] [Observability][RAC] Set display names for columns and fix reason message (#124570) * [Observability][RAC] Set display names for columns and fix reason message bug * Adding missing file * Adding a way to add additional fields to fetch to the timeline query --- .../pages/alerts/components/parse_alert.ts | 3 +- .../alerts_table_t_grid/add_display_names.ts | 29 +++++++++++++++++++ .../alerts_table_t_grid.tsx | 29 +++++++++++++++++-- .../components/t_grid/standalone/index.tsx | 9 ++++-- 4 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/add_display_names.ts diff --git a/x-pack/plugins/observability/public/pages/alerts/components/parse_alert.ts b/x-pack/plugins/observability/public/pages/alerts/components/parse_alert.ts index 2c07aed15122e..6577e022a2a6d 100644 --- a/x-pack/plugins/observability/public/pages/alerts/components/parse_alert.ts +++ b/x-pack/plugins/observability/public/pages/alerts/components/parse_alert.ts @@ -11,6 +11,7 @@ import { ALERT_STATUS_ACTIVE, ALERT_RULE_TYPE_ID, ALERT_RULE_NAME, + ALERT_REASON, } from '@kbn/rule-data-utils'; import type { TopAlert } from '../'; import { experimentalRuleFieldMap } from '../../../../../rule_registry/common/assets/field_maps/experimental_rule_field_map'; @@ -38,7 +39,7 @@ export const parseAlert = const formatter = observabilityRuleTypeRegistry.getFormatter(parsedFields[ALERT_RULE_TYPE_ID]!); const formatted = { link: undefined, - reason: parsedFields[ALERT_RULE_NAME] ?? '', + reason: parsedFields[ALERT_REASON] ?? parsedFields[ALERT_RULE_NAME] ?? '', ...(formatter?.({ fields: parsedFields, formatters: { asDuration, asPercent } }) ?? {}), }; diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/add_display_names.ts b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/add_display_names.ts new file mode 100644 index 0000000000000..d22daeba10ecb --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/add_display_names.ts @@ -0,0 +1,29 @@ +/* + * 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 { ALERT_DURATION, ALERT_REASON, ALERT_STATUS, TIMESTAMP } from '@kbn/rule-data-utils'; +import { EuiDataGridColumn } from '@elastic/eui'; +import { translations } from '../../../../config'; +import type { ColumnHeaderOptions } from '../../../../../../timelines/common'; + +export const addDisplayNames = ( + column: Pick & + ColumnHeaderOptions +) => { + if (column.id === ALERT_REASON) { + return { ...column, displayAsText: translations.alertsTable.reasonColumnDescription }; + } + if (column.id === ALERT_DURATION) { + return { ...column, displayAsText: translations.alertsTable.durationColumnDescription }; + } + if (column.id === ALERT_STATUS) { + return { ...column, displayAsText: translations.alertsTable.statusColumnDescription }; + } + if (column.id === TIMESTAMP) { + return { ...column, displayAsText: translations.alertsTable.lastUpdatedColumnDescription }; + } + return column; +}; diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx index f39a1be586d5f..d419fbee1d34e 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_table_t_grid/alerts_table_t_grid.tsx @@ -10,7 +10,18 @@ * We have types and code at different imports because we don't want to import the whole package in the resulting webpack bundle for the plugin. * This way plugins can do targeted imports to reduce the final code bundle */ -import { ALERT_DURATION, ALERT_REASON, ALERT_STATUS, TIMESTAMP } from '@kbn/rule-data-utils'; +import { + ALERT_DURATION, + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_REASON, + ALERT_RULE_CATEGORY, + ALERT_RULE_NAME, + ALERT_STATUS, + ALERT_UUID, + TIMESTAMP, + ALERT_START, +} from '@kbn/rule-data-utils'; import { EuiButtonIcon, @@ -53,6 +64,7 @@ import { LazyAlertsFlyout } from '../../../..'; import { parseAlert } from '../../components/parse_alert'; import { CoreStart } from '../../../../../../../../src/core/public'; import { translations, paths } from '../../../../config'; +import { addDisplayNames } from './add_display_names'; const ALERT_TABLE_STATE_STORAGE_KEY = 'xpack.observability.alert.tableState'; @@ -361,7 +373,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { casesOwner: observabilityFeatureId, casePermissions, type, - columns: tGridState?.columns ?? columns, + columns: (tGridState?.columns ?? columns).map(addDisplayNames), deletedEventIds, disabledCellActions: FIELDS_WITHOUT_CELL_ACTIONS, end: rangeTo, @@ -390,7 +402,18 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { sortDirection, }, ], - + queryFields: [ + ALERT_DURATION, + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_REASON, + ALERT_RULE_CATEGORY, + ALERT_RULE_NAME, + ALERT_STATUS, + ALERT_UUID, + ALERT_START, + TIMESTAMP, + ], leadingControlColumns, trailingControlColumns, unit: (totalAlerts: number) => translations.alertsTable.showingAlertsTitle(totalAlerts), diff --git a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx index 593278788e01c..0ee83bea1bc67 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx @@ -121,6 +121,7 @@ export interface TGridStandaloneProps { data?: DataPublicPluginStart; unit?: (total: number) => React.ReactNode; showCheckboxes?: boolean; + queryFields?: string[]; } const TGridStandaloneComponent: React.FC = ({ @@ -155,6 +156,7 @@ const TGridStandaloneComponent: React.FC = ({ data, unit, showCheckboxes = true, + queryFields = [], }) => { const dispatch = useDispatch(); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; @@ -166,7 +168,7 @@ const TGridStandaloneComponent: React.FC = ({ const { itemsPerPage: itemsPerPageStore, itemsPerPageOptions: itemsPerPageOptionsStore, - queryFields, + queryFields: queryFieldsFromState, sort: sortStore, title, } = useDeepEqualSelector((state) => getTGrid(state, STANDALONE_ID ?? '')); @@ -203,9 +205,9 @@ const TGridStandaloneComponent: React.FC = ({ (acc, c) => (c.linkField != null ? [...acc, c.id, c.linkField] : [...acc, c.id]), [] ), - ...(queryFields ?? []), + ...(queryFieldsFromState ?? []), ], - [columnsHeader, queryFields] + [columnsHeader, queryFieldsFromState] ); const sortField = useMemo( @@ -335,6 +337,7 @@ const TGridStandaloneComponent: React.FC = ({ sort, loadingText, unit, + queryFields, }) ); // eslint-disable-next-line react-hooks/exhaustive-deps From befefc334793a1d6121e427ba6c14323eed61e9e Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Tue, 8 Feb 2022 16:32:10 +0100 Subject: [PATCH 28/44] FTR - check ES security before creating system_indices_superuser (#124948) This PR adds a check if ES security is enabled before creating the system_indices_superuser in the security service. --- test/common/services/security/system_indices_user.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/common/services/security/system_indices_user.ts b/test/common/services/security/system_indices_user.ts index c1ab6b1e0abfa..2546fbeafffa7 100644 --- a/test/common/services/security/system_indices_user.ts +++ b/test/common/services/security/system_indices_user.ts @@ -25,6 +25,16 @@ export async function createSystemIndicesUser(ctx: FtrProviderContext) { const es = createEsClientForFtrConfig(config); + // There are cases where the test config file doesn't have security disabled + // but tests are still executed on ES without security. Checking this case + // by trying to fetch the users list. + try { + await es.security.getUser(); + } catch (error) { + log.debug('Could not fetch users, assuming security is disabled'); + return; + } + log.debug('===============creating system indices role and user==============='); await es.security.putRole({ From e7ab1e51c7f33a90009694de7a7fe810d95194b0 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 8 Feb 2022 10:43:44 -0500 Subject: [PATCH 29/44] skip failing test suite (#124938) --- x-pack/test/functional/apps/ml/permissions/full_ml_access.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts index c038aeba608bd..e41f1491f98af 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -21,7 +21,8 @@ export default function ({ getService }: FtrProviderContext) { { user: USER.ML_POWERUSER_SPACES, discoverAvailable: false }, ]; - describe('for user with full ML access', function () { + // Failing: See https://github.com/elastic/kibana/issues/124938 + describe.skip('for user with full ML access', function () { this.tags(['skipFirefox', 'mlqa']); describe('with no data loaded', function () { From 9a63bb813038b407d297f826b189e18b92c9da52 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 8 Feb 2022 10:31:28 -0600 Subject: [PATCH 30/44] [ci] Fix state check for purge cloud deployments (#124985) Pull request state from GitHub returns 'OPEN' if a pull request is open. This updates the capitalization when branching from state --- .buildkite/scripts/steps/cloud/purge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildkite/scripts/steps/cloud/purge.js b/.buildkite/scripts/steps/cloud/purge.js index b14a3be8f8daf..336f7daf736ae 100644 --- a/.buildkite/scripts/steps/cloud/purge.js +++ b/.buildkite/scripts/steps/cloud/purge.js @@ -26,7 +26,7 @@ for (const deployment of prDeployments) { const lastCommit = pullRequest.commits.slice(-1)[0]; const lastCommitTimestamp = new Date(lastCommit.committedDate).getTime() / 1000; - if (pullRequest.state !== 'open') { + if (pullRequest.state !== 'OPEN') { console.log(`Pull Request #${prNumber} is no longer open, will delete associated deployment`); deploymentsToPurge.push(deployment); } else if (!pullRequest.labels.filter((label) => label.name === 'ci:deploy-cloud')) { From e5678ffc5e3702932fbd175e9a5606b46fb32259 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Tue, 8 Feb 2022 10:54:46 -0600 Subject: [PATCH 31/44] [ML] Fix permission check for Discover/data view redirect from Anomaly detection explorer page (#124408) * Add permission check for data view redirect * Add checks if data view doesn't exist & tooltip hints * Remove console.error cause they are redundant * Not show link if no access to Discover * Fix duplicates * Fix duplicates, text Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/anomalies_table/links_menu.tsx | 60 ++++++++++++++----- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx index acca495a9c900..00ed14081c9be 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx @@ -15,6 +15,7 @@ import { EuiContextMenuPanel, EuiPopover, EuiProgress, + EuiToolTip, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; @@ -57,25 +58,33 @@ interface LinksMenuProps { export const LinksMenuUI = (props: LinksMenuProps) => { const [openInDiscoverUrl, setOpenInDiscoverUrl] = useState(); + const [discoverUrlError, setDiscoverUrlError] = useState(); const isCategorizationAnomalyRecord = isCategorizationAnomaly(props.anomaly); const closePopover = props.onItemClick; const kibana = useMlKibana(); + const { + services: { share, application }, + } = kibana; useEffect(() => { let unmounted = false; const generateDiscoverUrl = async () => { - const { - services: { share }, - } = kibana; const discoverLocator = share.url.locators.get('DISCOVER_APP_LOCATOR'); if (!discoverLocator) { - // eslint-disable-next-line no-console - console.error('No locator for Discover detected'); + const discoverLocatorMissing = i18n.translate( + 'xpack.ml.anomaliesTable.linksMenu.discoverLocatorMissingErrorMessage', + { + defaultMessage: 'No locator for Discover detected', + } + ); + + setDiscoverUrlError(discoverLocatorMissing); + return; } @@ -83,7 +92,20 @@ export const LinksMenuUI = (props: LinksMenuProps) => { const index = job.datafeed_config.indices[0]; const interval = props.interval; - const dataViewId = (await getDataViewIdFromName(index)) || index; + const dataViewId = await getDataViewIdFromName(index); + + // If data view doesn't exist for some reasons + if (!dataViewId) { + const autoGeneratedDiscoverLinkError = i18n.translate( + 'xpack.ml.anomaliesTable.linksMenu.autoGeneratedDiscoverLinkErrorMessage', + { + defaultMessage: `Unable to link to Discover; no data view exists for index '{index}'`, + values: { index }, + } + ); + + setDiscoverUrlError(autoGeneratedDiscoverLinkError); + } const record = props.anomaly.source; const earliestMoment = moment(record.timestamp).startOf(interval); @@ -243,9 +265,6 @@ export const LinksMenuUI = (props: LinksMenuProps) => { }; const viewSeries = async () => { - const { - services: { share }, - } = kibana; const mlLocator = share.url.locators.get(ML_APP_LOCATOR); const record = props.anomaly.source; @@ -501,22 +520,31 @@ export const LinksMenuUI = (props: LinksMenuProps) => { }); } - if (!isCategorizationAnomalyRecord) { + if (application.capabilities.discover?.show && !isCategorizationAnomalyRecord) { // Add item from the start, but disable it during the URL generation. - const isLoading = openInDiscoverUrl === undefined; + const isLoading = discoverUrlError === undefined && openInDiscoverUrl === undefined; items.push( - + {discoverUrlError ? ( + + + + ) : ( + + )} {isLoading ? : null} ); From 6f71ffcd0ed65f9e4654c5ea5548baf71031faec Mon Sep 17 00:00:00 2001 From: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com> Date: Tue, 8 Feb 2022 17:08:23 +0000 Subject: [PATCH 32/44] [Security Solution][Detections] fixes UX issue with discoverability of new features on the Rules page (#124343) - Added tour with 2 steps - Sorting/filtering for in memory table - New Bulk Edit actions - Created context with tour management, which wraps rules table - Results of user tour's journey is saved in localStorage - Disabled tour for Cypress tests **Note:** Text in second step was changed to "You can now bulk update index patterns and tags for multiple custom rules at once." It's not reflected on screenshots. On screenshots and video present older text: "You can now bulk update index patterns and tags for multiple rules at once.", where word `custom` is absent ## UI ### First step Screenshot 2022-02-03 at 16 54 20 ### Second step Screenshot 2022-02-03 at 16 54 29 Eui Tour: https://elastic.github.io/eui/#/display/tour ## Screen recording https://user-images.githubusercontent.com/92328789/152391296-0b03b299-270a-4d96-9adf-d98bc6405b4f.mov ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) --- .../security_solution/common/constants.ts | 3 + .../security_solution/cypress/tasks/login.ts | 29 +++- .../utility_bar/utility_bar_action.tsx | 11 +- .../all/bulk_actions/bulk_edit_flyout.tsx | 2 +- .../detection_engine/rules/all/index.test.tsx | 7 +- .../detection_engine/rules/all/index.tsx | 2 +- .../rules/all/optional_eui_tour_step.tsx | 29 ++++ .../rules/all/rules_feature_tour_context.tsx | 141 ++++++++++++++++++ .../rules/all/rules_table_toolbar.tsx | 33 +++- .../rules/all/utility_bar.tsx | 35 +++-- .../pages/detection_engine/rules/index.tsx | 138 ++++++++--------- .../detection_engine/rules/translations.ts | 44 ++++++ 12 files changed, 378 insertions(+), 96 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/optional_eui_tour_step.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_feature_tour_context.tsx diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 44e73cd8d1a8f..41dc63a2a48ec 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -422,3 +422,6 @@ export const LIMITED_CONCURRENCY_ROUTE_TAG_PREFIX = `${APP_ID}:limitedConcurrenc */ export const RULES_TABLE_MAX_PAGE_SIZE = 100; export const RULES_TABLE_PAGE_SIZE_OPTIONS = [5, 10, 20, 50, RULES_TABLE_MAX_PAGE_SIZE]; + +export const RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY = + 'securitySolution.rulesManagementPage.newFeaturesTour.v8.1'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/login.ts b/x-pack/plugins/security_solution/cypress/tasks/login.ts index ad6ad0486e518..349f2aaf32732 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/login.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/login.ts @@ -9,6 +9,7 @@ import * as yaml from 'js-yaml'; import Url, { UrlObject } from 'url'; import { ROLES } from '../../common/test'; +import { RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY } from '../../common/constants'; import { TIMELINE_FLYOUT_BODY } from '../screens/timeline'; import { hostDetailsUrl, LOGOUT_URL } from '../urls/navigation'; @@ -284,6 +285,21 @@ export const getEnvAuth = (): User => { } }; +/** + * Saves in localStorage rules feature tour config with deactivated option + * It prevents tour to appear during tests and cover UI elements + * @param window - browser's window object + */ +const disableRulesFeatureTour = (window: Window) => { + const tourConfig = { + isTourActive: false, + }; + window.localStorage.setItem( + RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY, + JSON.stringify(tourConfig) + ); +}; + /** * Authenticates with Kibana, visits the specified `url`, and waits for the * Kibana global nav to be displayed before continuing @@ -301,6 +317,7 @@ export const loginAndWaitForPage = ( if (onBeforeLoadCallback) { onBeforeLoadCallback(win); } + disableRulesFeatureTour(win); }, } ); @@ -315,13 +332,17 @@ export const waitForPage = (url: string) => { export const loginAndWaitForPageWithoutDateRange = (url: string, role?: ROLES) => { login(role); - cy.visit(role ? getUrlWithRoute(role, url) : url); + cy.visit(role ? getUrlWithRoute(role, url) : url, { + onBeforeLoad: disableRulesFeatureTour, + }); cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 }); }; export const loginWithUserAndWaitForPageWithoutDateRange = (url: string, user: User) => { loginWithUser(user); - cy.visit(constructUrlWithUser(user, url)); + cy.visit(constructUrlWithUser(user, url), { + onBeforeLoad: disableRulesFeatureTour, + }); cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 }); }; @@ -329,7 +350,9 @@ export const loginAndWaitForTimeline = (timelineId: string, role?: ROLES) => { const route = `/app/security/timelines?timeline=(id:'${timelineId}',isOpen:!t)`; login(role); - cy.visit(role ? getUrlWithRoute(role, route) : route); + cy.visit(role ? getUrlWithRoute(role, route) : route, { + onBeforeLoad: disableRulesFeatureTour, + }); cy.get('[data-test-subj="headerGlobalNav"]'); cy.get(TIMELINE_FLYOUT_BODY).should('be.visible'); }; diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx index faa4733a0bf3e..aa07a4442fab7 100644 --- a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx @@ -35,11 +35,17 @@ const Popover = React.memo( ownFocus, dataTestSubj, popoverPanelPaddingSize, + onClick, }) => { const [popoverState, setPopoverState] = useState(false); const closePopover = useCallback(() => setPopoverState(false), [setPopoverState]); + const handleLinkIconClick = useCallback(() => { + onClick?.(); + setPopoverState(!popoverState); + }, [popoverState, onClick]); + return ( ( iconSide={iconSide} iconSize={iconSize} iconType={iconType} - onClick={() => setPopoverState(!popoverState)} + onClick={handleLinkIconClick} disabled={disabled} > {children} } - closePopover={() => setPopoverState(false)} + closePopover={closePopover} isOpen={popoverState} repositionOnScroll > @@ -107,6 +113,7 @@ export const UtilityBarAction = React.memo( {popoverContent ? ( void; - onConfirm: (bulkactionEditPayload: BulkActionEditPayload) => void; + onConfirm: (bulkActionEditPayload: BulkActionEditPayload) => void; editAction: BulkActionEditType; rulesCount: number; tags: string[]; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx index 3b24dda539174..6092ec2a134d1 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx @@ -13,6 +13,7 @@ import { TestProviders } from '../../../../../common/mock'; import '../../../../../common/mock/formatted_relative'; import '../../../../../common/mock/match_media'; import { AllRules } from './index'; +import { RulesFeatureTourContextProvider } from './rules_feature_tour_context'; jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../../common/lib/kibana'); @@ -67,7 +68,8 @@ describe('AllRules', () => { rulesNotInstalled={0} rulesNotUpdated={0} /> - + , + { wrappingComponent: RulesFeatureTourContextProvider } ); await waitFor(() => { @@ -90,7 +92,8 @@ describe('AllRules', () => { rulesNotInstalled={0} rulesNotUpdated={0} /> - + , + { wrappingComponent: RulesFeatureTourContextProvider } ); await waitFor(() => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx index e8c7742125c74..6bb9927c8ab82 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx @@ -45,7 +45,7 @@ export const AllRules = React.memo( return ( <> - + = ({ + children, + stepProps, +}) => { + if (!stepProps) { + return <>{children}; + } + + return ( + + <>{children} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_feature_tour_context.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_feature_tour_context.tsx new file mode 100644 index 0000000000000..6c1d5a0de7a54 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_feature_tour_context.tsx @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { createContext, useContext, useEffect, useMemo, FC } from 'react'; + +import { noop } from 'lodash'; +import { + useEuiTour, + EuiTourState, + EuiStatelessTourStep, + EuiSpacer, + EuiButton, + EuiTourStepProps, +} from '@elastic/eui'; +import { invariant } from '../../../../../../common/utils/invariant'; +import { RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY } from '../../../../../../common/constants'; +import { useKibana } from '../../../../../common/lib/kibana'; + +import * as i18n from '../translations'; + +export interface RulesFeatureTourContextType { + steps: { + inMemoryTableStepProps: EuiTourStepProps; + bulkActionsStepProps: EuiTourStepProps; + }; + goToNextStep: () => void; + finishTour: () => void; +} + +const TOUR_POPOVER_WIDTH = 360; + +const featuresTourSteps: EuiStatelessTourStep[] = [ + { + step: 1, + title: i18n.FEATURE_TOUR_IN_MEMORY_TABLE_STEP_TITLE, + content: <>, + stepsTotal: 2, + children: <>, + onFinish: noop, + maxWidth: TOUR_POPOVER_WIDTH, + }, + { + step: 2, + title: i18n.FEATURE_TOUR_BULK_ACTIONS_STEP_TITLE, + content:

{i18n.FEATURE_TOUR_BULK_ACTIONS_STEP}

, + stepsTotal: 2, + children: <>, + onFinish: noop, + anchorPosition: 'rightUp', + maxWidth: TOUR_POPOVER_WIDTH, + }, +]; + +const tourConfig: EuiTourState = { + currentTourStep: 1, + isTourActive: true, + tourPopoverWidth: TOUR_POPOVER_WIDTH, + tourSubtitle: i18n.FEATURE_TOUR_TITLE, +}; + +const RulesFeatureTourContext = createContext(null); + +/** + * Context for new rules features, displayed in demo tour(euiTour) + * It has a common state in useEuiTour, which allows transition from one step to the next, for components within it[context] + * It also stores tour's state in localStorage + */ +export const RulesFeatureTourContextProvider: FC = ({ children }) => { + const { storage } = useKibana().services; + const initialStore = useMemo( + () => ({ + ...tourConfig, + ...(storage.get(RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY) ?? tourConfig), + }), + [storage] + ); + + const [stepProps, actions, reducerState] = useEuiTour(featuresTourSteps, initialStore); + + const finishTour = actions.finishTour; + const goToNextStep = actions.incrementStep; + + const inMemoryTableStepProps = useMemo( + () => ({ + ...stepProps[0], + content: ( + <> +

{i18n.FEATURE_TOUR_IN_MEMORY_TABLE_STEP}

+ + + {i18n.FEATURE_TOUR_IN_MEMORY_TABLE_STEP_NEXT} + + + ), + }), + [stepProps, goToNextStep] + ); + + useEffect(() => { + const { isTourActive, currentTourStep } = reducerState; + storage.set(RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY, { isTourActive, currentTourStep }); + }, [reducerState, storage]); + + const providerValue = useMemo( + () => ({ + steps: { + inMemoryTableStepProps, + bulkActionsStepProps: stepProps[1], + }, + finishTour, + goToNextStep, + }), + [finishTour, goToNextStep, inMemoryTableStepProps, stepProps] + ); + + return ( + + {children} + + ); +}; + +export const useRulesFeatureTourContext = (): RulesFeatureTourContextType => { + const rulesFeatureTourContext = useContext(RulesFeatureTourContext); + invariant( + rulesFeatureTourContext, + 'useRulesFeatureTourContext should be used inside RulesFeatureTourContextProvider' + ); + + return rulesFeatureTourContext; +}; + +export const useRulesFeatureTourContextOptional = (): RulesFeatureTourContextType | null => { + const rulesFeatureTourContext = useContext(RulesFeatureTourContext); + + return rulesFeatureTourContext; +}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_toolbar.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_toolbar.tsx index 261e14fd1411b..966cb726c8711 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_toolbar.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_toolbar.tsx @@ -10,6 +10,8 @@ import React from 'react'; import styled from 'styled-components'; import { useRulesTableContext } from './rules_table/rules_table_context'; import * as i18n from '../translations'; +import { useRulesFeatureTourContext } from './rules_feature_tour_context'; +import { OptionalEuiTourStep } from './optional_eui_tour_step'; const ToolbarLayout = styled.div` display: grid; @@ -22,6 +24,7 @@ const ToolbarLayout = styled.div` interface RulesTableToolbarProps { activeTab: AllRulesTabs; onTabChange: (tab: AllRulesTabs) => void; + loading: boolean; } export enum AllRulesTabs { @@ -43,12 +46,17 @@ const allRulesTabs = [ ]; export const RulesTableToolbar = React.memo( - ({ onTabChange, activeTab }) => { + ({ onTabChange, activeTab, loading }) => { const { state: { isInMemorySorting }, actions: { setIsInMemorySorting }, } = useRulesTableContext(); + const { + steps: { inMemoryTableStepProps }, + goToNextStep, + } = useRulesFeatureTourContext(); + return ( @@ -64,13 +72,22 @@ export const RulesTableToolbar = React.memo( ))} - - setIsInMemorySorting(e.target.checked)} - /> - + {/* delaying render of tour due to EuiPopover can't react to layout changes + https://github.com/elastic/kibana/pull/124343#issuecomment-1032467614 */} + + + { + if (inMemoryTableStepProps.isStepOpen) { + goToNextStep(); + } + setIsInMemorySorting(e.target.checked); + }} + /> + + ); } diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx index 6d9c2f92b214e..a936e84cee00a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx @@ -22,6 +22,9 @@ import { UtilityBarText, } from '../../../../../common/components/utility_bar'; import * as i18n from '../translations'; +import { useRulesFeatureTourContextOptional } from './rules_feature_tour_context'; + +import { OptionalEuiTourStep } from './optional_eui_tour_step'; interface AllRulesUtilityBarProps { canBulkEdit: boolean; @@ -55,6 +58,9 @@ export const AllRulesUtilityBar = React.memo( isBulkActionInProgress, hasDisabledActions, }) => { + // use optional rulesFeatureTourContext as AllRulesUtilityBar can be used outside the context + const featureTour = useRulesFeatureTourContextOptional(); + const handleGetBulkItemsPopoverContent = useCallback( (closePopover: () => void): JSX.Element | null => { if (onGetBulkItemsPopoverContent != null) { @@ -134,17 +140,24 @@ export const AllRulesUtilityBar = React.memo( )} {canBulkEdit && ( - - {i18n.BATCH_ACTIONS} - + + { + if (featureTour?.steps?.bulkActionsStepProps?.isStepOpen) { + featureTour?.finishTour(); + } + }} + > + {i18n.BATCH_ACTIONS} + + )} { showExceptionsCheckBox showCheckBox /> - - - - - {loadPrebuiltRulesAndTemplatesButton && ( - {loadPrebuiltRulesAndTemplatesButton} - )} - {reloadPrebuiltRulesAndTemplatesButton && ( - {reloadPrebuiltRulesAndTemplatesButton} - )} - - + + + + + + {loadPrebuiltRulesAndTemplatesButton && ( + {loadPrebuiltRulesAndTemplatesButton} + )} + {reloadPrebuiltRulesAndTemplatesButton && ( + {reloadPrebuiltRulesAndTemplatesButton} + )} + + + + {i18n.UPLOAD_VALUE_LISTS} + + + + - {i18n.UPLOAD_VALUE_LISTS} + {i18n.IMPORT_RULE} - - - - - {i18n.IMPORT_RULE} - - - - - {i18n.ADD_NEW_RULE} - - - - - {(prePackagedRuleStatus === 'ruleNeedUpdate' || - prePackagedTimelineStatus === 'timelineNeedUpdate') && ( - + + + {i18n.ADD_NEW_RULE} + + +
+ + {(prePackagedRuleStatus === 'ruleNeedUpdate' || + prePackagedTimelineStatus === 'timelineNeedUpdate') && ( + + )} + - )} - - - - + + + ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index 1de060c16a97a..386e00fc28d8b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -88,6 +88,50 @@ export const EDIT_PAGE_TITLE = i18n.translate( } ); +export const FEATURE_TOUR_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.tourTitle', + { + defaultMessage: "What's new", + } +); + +export const FEATURE_TOUR_IN_MEMORY_TABLE_STEP = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.inMemoryTableStepDescription', + { + defaultMessage: + 'The experimental rules table view allows for advanced sorting and filtering capabilities.', + } +); + +export const FEATURE_TOUR_IN_MEMORY_TABLE_STEP_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.inMemoryTableStepTitle', + { + defaultMessage: 'Step 1', + } +); + +export const FEATURE_TOUR_IN_MEMORY_TABLE_STEP_NEXT = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.inMemoryTableStepNextButtonTitle', + { + defaultMessage: 'Ok, got it', + } +); + +export const FEATURE_TOUR_BULK_ACTIONS_STEP_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.bulkActionsStepTitle', + { + defaultMessage: 'Step 2', + } +); + +export const FEATURE_TOUR_BULK_ACTIONS_STEP = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.bulkActionsStepDescription', + { + defaultMessage: + 'You can now bulk update index patterns and tags for multiple custom rules at once.', + } +); + export const REFRESH = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.refreshTitle', { From 370af2e64ba6d74da4e2d8a85d80db8734636016 Mon Sep 17 00:00:00 2001 From: Andrew Tate Date: Tue, 8 Feb 2022 11:25:58 -0600 Subject: [PATCH 33/44] [Lens] fix saving filters in Lens visualization (#124885) --- .../lens/public/app_plugin/app.test.tsx | 7 +++-- x-pack/plugins/lens/public/app_plugin/app.tsx | 13 ++++++--- .../app_plugin/lens_document_equality.ts | 16 ++++++++++- .../app_plugin/save_modal_container.tsx | 28 ++----------------- x-pack/plugins/lens/public/plugin.ts | 2 +- 5 files changed, 32 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 23804a8a6d618..9cf22b2a8fc84 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -678,13 +678,14 @@ describe('Lens App', () => { filters: [pinned, unpinned], }, }); + + const { state: expectedFilters } = services.data.query.filterManager.extract([unpinned]); + expect(services.attributeService.wrapAttributes).toHaveBeenCalledWith( expect.objectContaining({ savedObjectId: defaultSavedObjectId, title: 'hello there2', - state: expect.objectContaining({ - filters: services.data.query.filterManager.inject([unpinned], []), - }), + state: expect.objectContaining({ filters: expectedFilters }), }), true, { id: '5678', savedObjectId: defaultSavedObjectId } diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 78e739a6324ec..45517298b0432 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -92,9 +92,9 @@ export function App({ () => ({ datasourceMap, visualizationMap, - extractFilterReferences: data.query.filterManager.extract, + extractFilterReferences: data.query.filterManager.extract.bind(data.query.filterManager), }), - [datasourceMap, visualizationMap, data.query.filterManager.extract] + [datasourceMap, visualizationMap, data.query.filterManager] ); const currentDoc = useLensSelector((state) => @@ -153,7 +153,12 @@ export function App({ onAppLeave((actions) => { if ( application.capabilities.visualize.save && - !isLensEqual(persistedDoc, lastKnownDoc, data.query.filterManager.inject, datasourceMap) && + !isLensEqual( + persistedDoc, + lastKnownDoc, + data.query.filterManager.inject.bind(data.query.filterManager), + datasourceMap + ) && (isSaveable || persistedDoc) ) { return actions.confirm( @@ -174,7 +179,7 @@ export function App({ isSaveable, persistedDoc, application.capabilities.visualize.save, - data.query.filterManager.inject, + data.query.filterManager, datasourceMap, ]); diff --git a/x-pack/plugins/lens/public/app_plugin/lens_document_equality.ts b/x-pack/plugins/lens/public/app_plugin/lens_document_equality.ts index 3e833502c0592..59a8aadbe62ec 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_document_equality.ts +++ b/x-pack/plugins/lens/public/app_plugin/lens_document_equality.ts @@ -9,7 +9,7 @@ import { isEqual, intersection, union } from 'lodash'; import { FilterManager } from 'src/plugins/data/public'; import { Document } from '../persistence/saved_object_store'; import { DatasourceMap } from '../types'; -import { injectDocFilterReferences, removePinnedFilters } from './save_modal_container'; +import { removePinnedFilters } from './save_modal_container'; const removeNonSerializable = (obj: Parameters[0]) => JSON.parse(JSON.stringify(obj)); @@ -75,3 +75,17 @@ export const isLensEqual = ( return true; }; + +function injectDocFilterReferences( + injectFilterReferences: FilterManager['inject'], + doc?: Document +) { + if (!doc) return undefined; + return { + ...doc, + state: { + ...doc.state, + filters: injectFilterReferences(doc.state?.filters || [], doc.references), + }, + }; +} diff --git a/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx index e3098904a4b85..7fc03fd2a6551 100644 --- a/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx +++ b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx @@ -16,7 +16,6 @@ import type { LensAppProps, LensAppServices } from './types'; import type { SaveProps } from './app'; import { Document, checkForDuplicateTitle } from '../persistence'; import type { LensByReferenceInput, LensEmbeddableInput } from '../embeddable'; -import { FilterManager } from '../../../../../src/plugins/data/public'; import { APP_ID, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../common'; import { trackUiEvent } from '../lens_ui_telemetry'; import type { LensAppState } from '../state_management'; @@ -169,11 +168,10 @@ const redirectToDashboard = ({ const getDocToSave = ( lastKnownDoc: Document, saveProps: SaveProps, - references: SavedObjectReference[], - injectFilterReferences: FilterManager['inject'] + references: SavedObjectReference[] ) => { const docToSave = { - ...injectDocFilterReferences(injectFilterReferences, removePinnedFilters(lastKnownDoc))!, + ...removePinnedFilters(lastKnownDoc)!, references, }; @@ -201,7 +199,6 @@ export const runSaveLensVisualization = async ( ): Promise | undefined> => { const { chrome, - data, initialInput, originatingApp, lastKnownDoc, @@ -242,12 +239,7 @@ export const runSaveLensVisualization = async ( ); } - const docToSave = getDocToSave( - lastKnownDoc, - saveProps, - references, - data.query.filterManager.inject - ); + const docToSave = getDocToSave(lastKnownDoc, saveProps, references); // Required to serialize filters in by value mode until // https://github.com/elastic/kibana/issues/77588 is fixed @@ -358,20 +350,6 @@ export const runSaveLensVisualization = async ( } }; -export function injectDocFilterReferences( - injectFilterReferences: FilterManager['inject'], - doc?: Document -) { - if (!doc) return undefined; - return { - ...doc, - state: { - ...doc.state, - filters: injectFilterReferences(doc.state?.filters || [], doc.references), - }, - }; -} - export function removePinnedFilters(doc?: Document) { if (!doc) return undefined; return { diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index b3b78ffc4c2e8..8370d093a5b62 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -239,7 +239,7 @@ export class LensPlugin { timefilter: plugins.data.query.timefilter.timefilter, expressionRenderer: plugins.expressions.ReactExpressionRenderer, documentToExpression: this.editorFrameService!.documentToExpression, - injectFilterReferences: data.query.filterManager.inject, + injectFilterReferences: data.query.filterManager.inject.bind(data.query.filterManager), visualizationMap, indexPatternService: plugins.data.indexPatterns, uiActions: plugins.uiActions, From 30350fc9def38c0adf97ea25a875e23f5900e197 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Tue, 8 Feb 2022 18:28:48 +0100 Subject: [PATCH 34/44] [Fleet] Remove unwanted overflow on Integrations screenshots (#124975) --- .../sections/epm/screens/detail/overview/screenshots.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/screenshots.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/screenshots.tsx index d368b07b62e41..a6f11a40bc3b7 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/screenshots.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/screenshots.tsx @@ -5,6 +5,7 @@ * 2.0. */ import React, { useState, useMemo, memo } from 'react'; +import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiText, EuiPagination } from '@elastic/eui'; @@ -17,6 +18,9 @@ interface ScreenshotProps { packageName: string; version: string; } +const Pagination = styled(EuiPagination)` + max-width: 130px; +`; export const Screenshots: React.FC = memo(({ images, packageName, version }) => { const { toPackageImage } = useLinks(); @@ -48,7 +52,7 @@ export const Screenshots: React.FC = memo(({ images, packageNam - Date: Tue, 8 Feb 2022 18:31:21 +0100 Subject: [PATCH 35/44] Add failed-test and chore labels to Fleet project automation (#124994) --- .github/workflows/add-to-fleet-project.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/add-to-fleet-project.yml b/.github/workflows/add-to-fleet-project.yml index fc5676887f3ae..59b3513c85284 100644 --- a/.github/workflows/add-to-fleet-project.yml +++ b/.github/workflows/add-to-fleet-project.yml @@ -10,7 +10,9 @@ jobs: contains(github.event.issue.labels.*.name, 'Team:Fleet') && ( contains(github.event.issue.labels.*.name, 'technical debt') || contains(github.event.issue.labels.*.name, 'bug') || - contains(github.event.issue.labels.*.name, 'performance') + contains(github.event.issue.labels.*.name, 'performance') || + contains(github.event.issue.labels.*.name, 'failed-test') || + contains(github.event.issue.labels.*.name, 'chore') ) steps: - uses: octokit/graphql-action@v2.x @@ -28,5 +30,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: + # https://github.com/orgs/elastic/projects/763 PROJECT_ID: "PN_kwDOAGc3Zs4AAsH6" + # Token with `write:org` access GITHUB_TOKEN: ${{ secrets.FLEET_TECH_KIBANA_USER_TOKEN }} From ce094b970aaed6fcd029cf9bd180da0242d8b3c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Tue, 8 Feb 2022 18:39:10 +0100 Subject: [PATCH 36/44] [Security Solution][Endpoint] Set horizontal scrollbar to fix an issue with match_any operator (#124516) * Set max width 100% to fix an issue with match_any operator * Adds horizontal scrollbar for large criteria condition values * Adds horizontal scrollbar for large criteria condition values to the whole entry, not only on values Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/criteria_conditions.tsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/criteria_conditions.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/criteria_conditions.tsx index 048b79c354803..238fe87c05890 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/criteria_conditions.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/criteria_conditions.tsx @@ -146,15 +146,17 @@ export const CriteriaConditions = memo( {entries.map(({ field, type, value, operator, entries: nestedEntries = [] }) => { return (
- {CONDITION_AND}} - value={field} - color="subdued" - /> - +
+ {CONDITION_AND}} + value={field} + color="subdued" + /> + +
{getNestedEntriesContent(type, nestedEntries)}
); From 73cc08e0755bc1a409e3e4a03870e764a3bbf145 Mon Sep 17 00:00:00 2001 From: Tobias Stadler Date: Tue, 8 Feb 2022 18:51:39 +0100 Subject: [PATCH 37/44] Rename Backend to Dependency (#124067) * Rename Backend to Dependency which was missed in #110523 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../waterfall/span_flyout/sticky_span_properties.tsx | 12 ++++++------ x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx index 3067b335f4861..1e8cf5d19e5e0 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx @@ -49,7 +49,7 @@ export function StickySpanProperties({ span, transaction }: Props) { }); const spanName = span.span.name; - const backendName = span.span.destination?.service.resource; + const dependencyName = span.span.destination?.service.resource; const transactionStickyProperties = transaction ? [ @@ -98,13 +98,13 @@ export function StickySpanProperties({ span, transaction }: Props) { ] : []; - const backendStickyProperties = backendName + const dependencyStickyProperties = dependencyName ? [ { label: i18n.translate( - 'xpack.apm.transactionDetails.spanFlyout.backendLabel', + 'xpack.apm.transactionDetails.spanFlyout.dependencyLabel', { - defaultMessage: 'Backend', + defaultMessage: 'Dependency', } ), fieldName: SPAN_DESTINATION_SERVICE_RESOURCE, @@ -112,7 +112,7 @@ export function StickySpanProperties({ span, transaction }: Props) { Date: Tue, 8 Feb 2022 15:15:34 -0500 Subject: [PATCH 38/44] [Fleet] Fix preconfiguration error when renaming a preconfigured policy (#124953) * Fix preconfiguration error when renaming a preconfigured policy * Add test + only compare on ID if it's defined on the preconfigured policy Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/services/preconfiguration.test.ts | 50 +++++++++++++++++++ .../fleet/server/services/preconfiguration.ts | 4 +- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index 89b8098f477fd..6d6d641381da2 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -283,6 +283,7 @@ const spyAgentPolicyServicBumpAllAgentPoliciesForOutput = jest.spyOn( describe('policy preconfiguration', () => { beforeEach(() => { + mockedPackagePolicyService.getByIDs.mockReset(); mockedPackagePolicyService.create.mockReset(); mockInstalledPackages.clear(); mockInstallPackageErrors.clear(); @@ -468,6 +469,55 @@ describe('policy preconfiguration', () => { ); }); + it('should not try to recreate preconfigure package policy that has been renamed', async () => { + const soClient = getPutPreconfiguredPackagesMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + mockedPackagePolicyService.getByIDs.mockResolvedValue([ + { name: 'Renamed package policy', id: 'test_package1' } as PackagePolicy, + ]); + + mockConfiguredPolicies.set('test-id', { + name: 'Test policy', + description: 'Test policy description', + unenroll_timeout: 120, + namespace: 'default', + id: 'test-id', + package_policies: [ + { + name: 'test_package1', + id: 'test_package1', + }, + ], + is_managed: true, + } as PreconfiguredAgentPolicy); + + await ensurePreconfiguredPackagesAndPolicies( + soClient, + esClient, + [ + { + name: 'Test policy', + namespace: 'default', + id: 'test-id', + is_managed: true, + package_policies: [ + { + package: { name: 'test_package' }, + name: 'test_package1', + id: 'test_package1', + }, + ], + }, + ] as PreconfiguredAgentPolicy[], + [{ name: 'test_package', version: '3.0.0' }], + mockDefaultOutput, + DEFAULT_SPACE_ID + ); + + expect(mockedPackagePolicyService.create).not.toBeCalled(); + }); + it('should throw an error when trying to install duplicate packages', async () => { const soClient = getPutPreconfiguredPackagesMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index 06d700c3577b5..e9d97856a926f 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -338,7 +338,9 @@ export async function ensurePreconfiguredPackagesAndPolicies( const packagePoliciesToAdd = installedPackagePolicies.filter((installablePackagePolicy) => { return !(agentPolicyWithPackagePolicies?.package_policies as PackagePolicy[]).some( - (packagePolicy) => packagePolicy.name === installablePackagePolicy.name + (packagePolicy) => + (packagePolicy.id !== undefined && packagePolicy.id === installablePackagePolicy.id) || + packagePolicy.name === installablePackagePolicy.name ); }); From 1f3b7f405e7a8bd2c30c1a6e02d03f540b10f088 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 8 Feb 2022 15:19:37 -0500 Subject: [PATCH 39/44] skip failing test suite (#124990) --- test/functional/apps/discover/_saved_queries.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts index 5a8b14545508c..fe94987289a39 100644 --- a/test/functional/apps/discover/_saved_queries.ts +++ b/test/functional/apps/discover/_saved_queries.ts @@ -36,7 +36,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await queryBar.setQuery('response:200'); }; - describe('saved queries saved objects', function describeIndexTests() { + // Failing: See https://github.com/elastic/kibana/issues/124990 + describe.skip('saved queries saved objects', function describeIndexTests() { before(async function () { log.debug('load kibana index with default index pattern'); await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); From 1af9629c58de827d34694d5f7e132ba7abade817 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 8 Feb 2022 20:58:01 +0000 Subject: [PATCH 40/44] chore(NA): splits types from code on @kbn/typed-react-router-config (#124944) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 1 + packages/BUILD.bazel | 1 + .../kbn-typed-react-router-config/BUILD.bazel | 39 +++++++++++++++---- .../package.json | 1 - yarn.lock | 4 ++ 5 files changed, 38 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 9ea3e8335f886..d284cfbc87648 100644 --- a/package.json +++ b/package.json @@ -618,6 +618,7 @@ "@types/kbn__telemetry-tools": "link:bazel-bin/packages/kbn-telemetry-tools/npm_module_types", "@types/kbn__test": "link:bazel-bin/packages/kbn-test/npm_module_types", "@types/kbn__test-jest-helpers": "link:bazel-bin/packages/kbn-test-jest-helpers/npm_module_types", + "@types/kbn__typed-react-router-config": "link:bazel-bin/packages/kbn-typed-react-router-config/npm_module_types", "@types/kbn__ui-shared-deps-npm": "link:bazel-bin/packages/kbn-ui-shared-deps-npm/npm_module_types", "@types/kbn__ui-shared-deps-src": "link:bazel-bin/packages/kbn-ui-shared-deps-src/npm_module_types", "@types/kbn__ui-theme": "link:bazel-bin/packages/kbn-ui-theme/npm_module_types", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 6421f36bf73b7..02e82476cd88d 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -132,6 +132,7 @@ filegroup( "//packages/kbn-telemetry-tools:build_types", "//packages/kbn-test:build_types", "//packages/kbn-test-jest-helpers:build_types", + "//packages/kbn-typed-react-router-config:build_types", "//packages/kbn-ui-shared-deps-npm:build_types", "//packages/kbn-ui-shared-deps-src:build_types", "//packages/kbn-ui-theme:build_types", diff --git a/packages/kbn-typed-react-router-config/BUILD.bazel b/packages/kbn-typed-react-router-config/BUILD.bazel index 6f4e53e58fff7..62fd6adf5bb26 100644 --- a/packages/kbn-typed-react-router-config/BUILD.bazel +++ b/packages/kbn-typed-react-router-config/BUILD.bazel @@ -1,9 +1,10 @@ -load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") -load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") -load("//src/dev/bazel:index.bzl", "jsts_transpiler") +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") PKG_BASE_NAME = "kbn-typed-react-router-config" PKG_REQUIRE_NAME = "@kbn/typed-react-router-config" +TYPES_PKG_REQUIRE_NAME = "@types/kbn__typed-react-router-config" SOURCE_FILES = glob( [ @@ -28,23 +29,30 @@ NPM_MODULE_EXTRA_FILES = [ RUNTIME_DEPS = [ "//packages/kbn-io-ts-utils", - "@npm//tslib", - "@npm//utility-types", + "@npm//fp-ts", + "@npm//history", "@npm//io-ts", + "@npm//lodash", "@npm//query-string", + "@npm//react", "@npm//react-router-config", "@npm//react-router-dom", + "@npm//tslib", + "@npm//utility-types", ] TYPES_DEPS = [ "//packages/kbn-io-ts-utils:npm_module_types", + "@npm//fp-ts", "@npm//query-string", "@npm//utility-types", + "@npm//@types/history", "@npm//@types/jest", + "@npm//@types/lodash", "@npm//@types/node", + "@npm//@types/react", "@npm//@types/react-router-config", "@npm//@types/react-router-dom", - "@npm//@types/history", ] jsts_transpiler( @@ -86,7 +94,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node", ":target_web", ":tsc_types"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) @@ -105,3 +113,20 @@ filegroup( ], visibility = ["//visibility:public"], ) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = TYPES_PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [ + ":npm_module_types", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-typed-react-router-config/package.json b/packages/kbn-typed-react-router-config/package.json index 50c2e4b5d7e89..0f45f63f4ab2d 100644 --- a/packages/kbn-typed-react-router-config/package.json +++ b/packages/kbn-typed-react-router-config/package.json @@ -1,7 +1,6 @@ { "name": "@kbn/typed-react-router-config", "main": "target_node/index.js", - "types": "target_types/index.d.ts", "browser": "target_web/index.js", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", diff --git a/yarn.lock b/yarn.lock index ad5df52a1655c..18bb281349a43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6929,6 +6929,10 @@ version "0.0.0" uid "" +"@types/kbn__typed-react-router-config@link:bazel-bin/packages/kbn-typed-react-router-config/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__ui-shared-deps-npm@link:bazel-bin/packages/kbn-ui-shared-deps-npm/npm_module_types": version "0.0.0" uid "" From ec323617b5a394686ef45908e82786f99dce6b2a Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Tue, 8 Feb 2022 14:37:22 -0700 Subject: [PATCH 41/44] Adds section on Advanced Settings (#124915) Co-authored-by: Kaarina Tungseth --- dev_docs/key_concepts/building_blocks.mdx | 6 + dev_docs/tutorials/advanced_settings.mdx | 288 ++++++++++++++++++++++ 2 files changed, 294 insertions(+) create mode 100644 dev_docs/tutorials/advanced_settings.mdx diff --git a/dev_docs/key_concepts/building_blocks.mdx b/dev_docs/key_concepts/building_blocks.mdx index 6536019f668cf..61e3a711775c3 100644 --- a/dev_docs/key_concepts/building_blocks.mdx +++ b/dev_docs/key_concepts/building_blocks.mdx @@ -122,6 +122,12 @@ sharing and space isolation, and tags. **Github labels**: `Team:Core`, `Feature:Saved Objects` +## Advanced Settings + + + - + - + - + +`uiSettings` are registered synchronously during `core`'s setup lifecycle phase. This means that once you add a new advanced setting, you cannot change or remove it without . + +### Configuration with Advanced Settings UI + +The `uiSettings` service is the programmatic interface to Kibana's Advanced Settings UI. Kibana plugins use the service to extend Kibana UI Settings Management with custom settings for a plugin. + +Configuration through the Advanced Settings UI is restricted to users authorised to access the Advanced Settings page. Users who don't have permissions to change these values default to using the csettings configuration defined for the space that they are in. The `config` saved object can be shared between spaces. + +### Configuration with UI settings overrides + +When a setting is configured as an override in `kibana.yml`, it overrides any other value store in the `config` saved object. The override applies to Kibana as a whole for all spaces and users, and the option is disabled on the Advanced Settings page. We refer to these as "global" overrides. + +Note: If an override is misconfigured, it fails config validation and prevents Kibana from starting up. Validation is, however, limited to value _type_ and not to _key_ (name). For example, when a plugin registers the `my_plugin_foo: 42` setting , then declares the following override, the config validation fails: + +```kibana.yml +uiSettings.overrides: + my_plugin_foo: "42" +``` +The following override results in a successful config validation: + +```kibana.yml +uiSettings.overrides: + my_pluginFoo: 42 +``` + +### Client side usage + +On the client, the `uiSettings` service is accessible directly from `core` and the client provides plugins access to the `config` entries stored in Elasticsearch. + + + Refer to [the client-side uiSettings service API docs](https://github.com/elastic/kibana/blob/main/docs/development/core/server/kibana-plugin-core-public.iuisettingsclient.md) + + +The following is a basic example for using the `uiSettings` service: + +**src/plugins/charts/public/plugin.ts** +```ts +import { Plugin, CoreSetup } from 'kibana/public'; +import { ExpressionsSetup } from '../../expressions/public'; +import { palette, systemPalette } from '../common'; + +import { ThemeService, LegacyColorsService } from './services'; +import { PaletteService } from './services/palettes/service'; +import { ActiveCursor } from './services/active_cursor'; + +export type Theme = Omit; +export type Color = Omit; + +interface SetupDependencies { + expressions: ExpressionsSetup; +} + +/** @public */ +export interface ChartsPluginSetup { + legacyColors: Color; + theme: Theme; + palettes: ReturnType; +} + +/** @public */ +export type ChartsPluginStart = ChartsPluginSetup & { + activeCursor: ActiveCursor; +}; + +/** @public */ +export class ChartsPlugin implements Plugin { + private readonly themeService = new ThemeService(); + private readonly legacyColorsService = new LegacyColorsService(); + private readonly paletteService = new PaletteService(); + private readonly activeCursor = new ActiveCursor(); + + private palettes: undefined | ReturnType; + + public setup(core: CoreSetup, dependencies: SetupDependencies): ChartsPluginSetup { + dependencies.expressions.registerFunction(palette); + dependencies.expressions.registerFunction(systemPalette); + this.themeService.init(core.uiSettings); + this.legacyColorsService.init(core.uiSettings); + this.palettes = this.paletteService.setup(this.legacyColorsService); + + this.activeCursor.setup(); + + return { + legacyColors: this.legacyColorsService, + theme: this.themeService, + palettes: this.palettes, + }; + } + + public start(): ChartsPluginStart { + return { + legacyColors: this.legacyColorsService, + theme: this.themeService, + palettes: this.palettes!, + activeCursor: this.activeCursor, + }; + } +} + +``` + +### Server side usage + +On the server side, `uiSettings` are accessible directly from `core`. The following example shows how to register a new setting with the minimum required schema parameter against which validations are performed on read and write. +The example also shows how plugins can leverage the optional deprecation parameter on registration for handling deprecation notices and renames. The deprecation warnings are rendered in the Advanced Settings UI and should also be added to the Configure Kibana guide. + + + Refer to [the server-side uiSettings service API docs](https://github.com/elastic/kibana/blob/main/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md) + + +**src/plugins/charts/server/plugin.ts** + +```ts +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { CoreSetup, Plugin } from 'kibana/server'; +import { COLOR_MAPPING_SETTING, LEGACY_TIME_AXIS, palette, systemPalette } from '../common'; +import { ExpressionsServerSetup } from '../../expressions/server'; + +interface SetupDependencies { + expressions: ExpressionsServerSetup; +} + +export class ChartsServerPlugin implements Plugin { + public setup(core: CoreSetup, dependencies: SetupDependencies) { + dependencies.expressions.registerFunction(palette); + dependencies.expressions.registerFunction(systemPalette); + core.uiSettings.register({ + [COLOR_MAPPING_SETTING]: { + name: i18n.translate('charts.advancedSettings.visualization.colorMappingTitle', { + defaultMessage: 'Color mapping', + }), + value: JSON.stringify({ + Count: '#00A69B', + }), + type: 'json', + description: i18n.translate('charts.advancedSettings.visualization.colorMappingText', { + defaultMessage: + 'Maps values to specific colors in charts using the Compatibility palette.', + }), + deprecation: { + message: i18n.translate( + 'charts.advancedSettings.visualization.colorMappingTextDeprecation', + { + defaultMessage: + 'This setting is deprecated and will not be supported in a future version.', + } + ), + docLinksKey: 'visualizationSettings', + }, + category: ['visualization'], + schema: schema.string(), + }, + ... + }); + + return {}; + } + + public start() { + return {}; + } + + public stop() {} +} +``` +For optimal Kibana performance, `uiSettings` are cached. Any changes that require a cache refresh should use the `requiresPageReload` parameter on registration. + +For example, changing the time filter refresh interval triggers a prompt in the UI that the page needs to be refreshed to save the new value: + +**src/plugins/data/server/ui_settings.ts** + +```ts +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import type { DocLinksServiceSetup, UiSettingsParams } from 'kibana/server'; +import { DEFAULT_QUERY_LANGUAGE, UI_SETTINGS } from '../common'; + +export function getUiSettings( + docLinks: DocLinksServiceSetup +): Record> { + return { + ... + [UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS]: { + name: i18n.translate('data.advancedSettings.timepicker.refreshIntervalDefaultsTitle', { + defaultMessage: 'Time filter refresh interval', + }), + value: `{ + "pause": false, + "value": 0 + }`, + type: 'json', + description: i18n.translate('data.advancedSettings.timepicker.refreshIntervalDefaultsText', { + defaultMessage: `The timefilter's default refresh interval. The "value" needs to be specified in milliseconds.`, + }), + requiresPageReload: true, + schema: schema.object({ + pause: schema.boolean(), + value: schema.number(), + }), + }, + ... + } +} +``` + +### Registering Migrations +To change or remove a `uiSetting`, you must migrate the whole `config` Saved Object. `uiSettings` migrations are declared directly in the service. + +For example, in 7.9.0, `siem` as renamed to `securitySolution`, and in 8.0.0, `theme:version` was removed: + +**src/core/server/ui_settings/saved_objects/migrations.ts** + +```ts +import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc } from 'kibana/server'; + +export const migrations = { + '7.9.0': (doc: SavedObjectUnsanitizedDoc): SavedObjectSanitizedDoc => ({ + ...doc, + ...(doc.attributes && { + attributes: Object.keys(doc.attributes).reduce( + (acc, key) => + key.startsWith('siem:') + ? { + ...acc, + [key.replace('siem', 'securitySolution')]: doc.attributes[key], + } + : { + ...acc, + [key]: doc.attributes[key], + }, + {} + ), + }), + references: doc.references || [], + }), + '7.12.0': (doc: SavedObjectUnsanitizedDoc): SavedObjectSanitizedDoc => ({...}), + '7.13.0': (doc: SavedObjectUnsanitizedDoc): SavedObjectSanitizedDoc => ({...}), + '8.0.0': (doc: SavedObjectUnsanitizedDoc): SavedObjectSanitizedDoc => ({ + ...doc, + ...(doc.attributes && { + attributes: Object.keys(doc.attributes).reduce( + (acc, key) => + [ + // owner: Team:Geo [1] + 'visualization:regionmap:showWarnings', + ... + // owner: Team:Core + ... + 'theme:version', + // owner: Team:AppServices + ... + ].includes(key) + ? { + ...acc, + } + : { + ...acc, + [key]: doc.attributes[key], + }, + {} + ), + }), + references: doc.references || [], + }), + '8.1.0': (doc: SavedObjectUnsanitizedDoc): SavedObjectSanitizedDoc => ({...}), +}; +``` +[1] Since all `uiSettings` migrations are added to the same migration function, while not required, grouping settings by team is good practice. From 330c3e21acbcf4fc65312e5e39e360466fc02fdd Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Tue, 8 Feb 2022 15:41:52 -0600 Subject: [PATCH 42/44] [data views] don't allow single `*` index pattern (#124906) * don't allow single astrisk * Update form_schema.ts * add tests --- .../public/components/form_schema.test.ts | 22 +++++++++++++++++++ .../public/components/form_schema.ts | 17 +++++++++++++- .../data_view_editor/public/shared_imports.ts | 1 + 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 src/plugins/data_view_editor/public/components/form_schema.test.ts diff --git a/src/plugins/data_view_editor/public/components/form_schema.test.ts b/src/plugins/data_view_editor/public/components/form_schema.test.ts new file mode 100644 index 0000000000000..b2e1f697843c6 --- /dev/null +++ b/src/plugins/data_view_editor/public/components/form_schema.test.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { singleAstriskValidator } from './form_schema'; +import { ValidationFuncArg } from '../shared_imports'; + +describe('validators', () => { + test('singleAstriskValidator should pass', async () => { + const result = singleAstriskValidator({ value: 'kibana*' } as ValidationFuncArg); + expect(result).toBeUndefined(); + }); + test('singleAstriskValidator should fail', async () => { + const result = singleAstriskValidator({ value: '*' } as ValidationFuncArg); + // returns error + expect(result).toBeDefined(); + }); +}); diff --git a/src/plugins/data_view_editor/public/components/form_schema.ts b/src/plugins/data_view_editor/public/components/form_schema.ts index a6df0c4206d2a..178fedda2de34 100644 --- a/src/plugins/data_view_editor/public/components/form_schema.ts +++ b/src/plugins/data_view_editor/public/components/form_schema.ts @@ -7,9 +7,21 @@ */ import { i18n } from '@kbn/i18n'; -import { fieldValidators } from '../shared_imports'; +import { fieldValidators, ValidationFunc } from '../shared_imports'; import { INDEX_PATTERN_TYPE } from '../types'; +export const singleAstriskValidator = ( + ...args: Parameters +): ReturnType => { + const [{ value, path }] = args; + + const message = i18n.translate('indexPatternEditor.validations.noSingleAstriskPattern', { + defaultMessage: "A single '*' is not an allowed index pattern", + }); + + return value === '*' ? { code: 'ERR_FIELD_MISSING', path, message } : undefined; +}; + export const schema = { title: { label: i18n.translate('indexPatternEditor.editor.form.titleLabel', { @@ -28,6 +40,9 @@ export const schema = { }) ), }, + { + validator: singleAstriskValidator, + }, ], }, timestampField: { diff --git a/src/plugins/data_view_editor/public/shared_imports.ts b/src/plugins/data_view_editor/public/shared_imports.ts index cca695bc9a95e..dd9b8ea2a0e41 100644 --- a/src/plugins/data_view_editor/public/shared_imports.ts +++ b/src/plugins/data_view_editor/public/shared_imports.ts @@ -28,6 +28,7 @@ export type { ValidationFunc, FieldConfig, ValidationConfig, + ValidationFuncArg, } from '../../es_ui_shared/static/forms/hook_form_lib'; export { useForm, From 3249e565a64e5650c4f5c0e2f50df5c9cc5fbe05 Mon Sep 17 00:00:00 2001 From: nastasha-solomon <79124755+nastasha-solomon@users.noreply.github.com> Date: Tue, 8 Feb 2022 16:57:29 -0500 Subject: [PATCH 43/44] [DOCS] Pre-configured connectors can no longer be used within Cases (#123876) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/management/connectors/pre-configured-connectors.asciidoc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/management/connectors/pre-configured-connectors.asciidoc b/docs/management/connectors/pre-configured-connectors.asciidoc index 4d304cdd6c5a2..aaef1b673d0b6 100644 --- a/docs/management/connectors/pre-configured-connectors.asciidoc +++ b/docs/management/connectors/pre-configured-connectors.asciidoc @@ -11,6 +11,8 @@ action are predefined, including the connector name and ID. - Appear in all spaces because they are not saved objects. - Cannot be edited or deleted. +NOTE: Preconfigured connectors cannot be used with cases. + [float] [[preconfigured-connector-example]] ==== Preconfigured connectors example @@ -70,4 +72,4 @@ image::images/pre-configured-connectors-managing.png[Connectors managing tab wit Clicking a preconfigured connector shows the description, but not the configuration. A message indicates that this is a preconfigured connector. [role="screenshot"] -image::images/pre-configured-connectors-view-screen.png[Pre-configured connector view details] \ No newline at end of file +image::images/pre-configured-connectors-view-screen.png[Pre-configured connector view details] From 16292de73035bf986eb10428f66611cb33dec3d6 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Tue, 8 Feb 2022 17:15:50 -0500 Subject: [PATCH 44/44] move awaitingRemoval control variable (#124913) --- .../public/application/lib/sync_dashboard_url_state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/dashboard/public/application/lib/sync_dashboard_url_state.ts b/src/plugins/dashboard/public/application/lib/sync_dashboard_url_state.ts index e0a1526baa473..392b37bb4d8e0 100644 --- a/src/plugins/dashboard/public/application/lib/sync_dashboard_url_state.ts +++ b/src/plugins/dashboard/public/application/lib/sync_dashboard_url_state.ts @@ -94,13 +94,13 @@ const loadDashboardUrlState = ({ if (!awaitingRemoval) { awaitingRemoval = true; kbnUrlStateStorage.kbnUrlControls.updateAsync((nextUrl) => { + awaitingRemoval = false; if (nextUrl.includes(DASHBOARD_STATE_STORAGE_KEY)) { return replaceUrlHashQuery(nextUrl, (query) => { delete query[DASHBOARD_STATE_STORAGE_KEY]; return query; }); } - awaitingRemoval = false; return nextUrl; }, true); }