diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts index fc26c837d5e52..267d671361184 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts @@ -96,8 +96,7 @@ describe('getSearchDsl', () => { mappings, opts.type, opts.sortField, - opts.sortOrder, - opts.pit + opts.sortOrder ); }); diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index cae5e43897bcf..9820544f02bd1 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -78,7 +78,7 @@ export function getSearchDsl( hasReferenceOperator, kueryNode, }), - ...getSortingParams(mappings, type, sortField, sortOrder, pit), + ...getSortingParams(mappings, type, sortField, sortOrder), ...(pit ? getPitParams(pit) : {}), ...(searchAfter ? { search_after: searchAfter } : {}), }; diff --git a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts index 73c7065705fc5..1376f0d50a9da 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts @@ -79,11 +79,6 @@ describe('searchDsl/getSortParams', () => { ], }); }); - it('appends tiebreaker when PIT is provided', () => { - expect(getSortingParams(MAPPINGS, 'saved', 'title', undefined, { id: 'abc' }).sort).toEqual( - expect.arrayContaining([{ _shard_doc: 'asc' }]) - ); - }); }); describe('sortField is simple root property with multiple types', () => { it('returns correct params', () => { @@ -98,11 +93,6 @@ describe('searchDsl/getSortParams', () => { ], }); }); - it('appends tiebreaker when PIT is provided', () => { - expect( - getSortingParams(MAPPINGS, ['saved', 'pending'], 'type', undefined, { id: 'abc' }).sort - ).toEqual(expect.arrayContaining([{ _shard_doc: 'asc' }])); - }); }); describe('sortField is simple non-root property with multiple types', () => { it('returns correct params', () => { @@ -124,11 +114,6 @@ describe('searchDsl/getSortParams', () => { ], }); }); - it('appends tiebreaker when PIT is provided', () => { - expect( - getSortingParams(MAPPINGS, 'saved', 'title.raw', undefined, { id: 'abc' }).sort - ).toEqual(expect.arrayContaining([{ _shard_doc: 'asc' }])); - }); }); describe('sortField is multi-field with single type as array', () => { it('returns correct params', () => { @@ -143,11 +128,6 @@ describe('searchDsl/getSortParams', () => { ], }); }); - it('appends tiebreaker when PIT is provided', () => { - expect( - getSortingParams(MAPPINGS, ['saved'], 'title.raw', undefined, { id: 'abc' }).sort - ).toEqual(expect.arrayContaining([{ _shard_doc: 'asc' }])); - }); }); describe('sortField is root multi-field with multiple types', () => { it('returns correct params', () => { @@ -162,12 +142,6 @@ describe('searchDsl/getSortParams', () => { ], }); }); - it('appends tiebreaker when PIT is provided', () => { - expect( - getSortingParams(MAPPINGS, ['saved', 'pending'], 'type.raw', undefined, { id: 'abc' }) - .sort - ).toEqual(expect.arrayContaining([{ _shard_doc: 'asc' }])); - }); }); describe('sortField is not-root multi-field with multiple types', () => { it('returns correct params', () => { diff --git a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts index abef9bfa0a300..e3bfba6a80f59 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts @@ -8,12 +8,6 @@ import Boom from '@hapi/boom'; import { getProperty, IndexMapping } from '../../../mappings'; -import { SavedObjectsPitParams } from '../../../types'; - -// TODO: The plan is for ES to automatically add this tiebreaker when -// using PIT. We should remove this logic once that is resolved. -// https://github.com/elastic/elasticsearch/issues/56828 -const ES_PROVIDED_TIEBREAKER = { _shard_doc: 'asc' }; const TOP_LEVEL_FIELDS = ['_id', '_score']; @@ -21,8 +15,7 @@ export function getSortingParams( mappings: IndexMapping, type: string | string[], sortField?: string, - sortOrder?: string, - pit?: SavedObjectsPitParams + sortOrder?: string ) { if (!sortField) { return {}; @@ -38,7 +31,6 @@ export function getSortingParams( order: sortOrder, }, }, - ...(pit ? [ES_PROVIDED_TIEBREAKER] : []), ], }; } @@ -59,7 +51,6 @@ export function getSortingParams( unmapped_type: rootField.type, }, }, - ...(pit ? [ES_PROVIDED_TIEBREAKER] : []), ], }; } @@ -84,7 +75,6 @@ export function getSortingParams( unmapped_type: field.type, }, }, - ...(pit ? [ES_PROVIDED_TIEBREAKER] : []), ], }; } diff --git a/src/plugins/embeddable/README.asciidoc b/src/plugins/embeddable/README.asciidoc index daa6040eab7eb..165dc37c56cb3 100644 --- a/src/plugins/embeddable/README.asciidoc +++ b/src/plugins/embeddable/README.asciidoc @@ -22,18 +22,17 @@ There is also an example of rendering dashboard container outside of dashboard a === Docs -link:./docs/README.md[Embeddable docs, guides & caveats] +link:https://github.com/elastic/kibana/blob/master/src/plugins/embeddable/docs/README.md[Embeddable docs, guides & caveats] === API docs -==== Server API -https://github.com/elastic/kibana/blob/master/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md[Server Setup contract] -https://github.com/elastic/kibana/blob/master/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablestart.md[Server Start contract] - -===== Browser API +==== Browser API https://github.com/elastic/kibana/blob/master/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablesetup.md[Browser Setup contract] https://github.com/elastic/kibana/blob/master/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestart.md[Browser Start contract] +==== Server API +https://github.com/elastic/kibana/blob/master/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md[Server Setup contract] + === Testing Run unit tests diff --git a/src/plugins/expressions/README.asciidoc b/src/plugins/expressions/README.asciidoc index e07f6e2909ab8..554e3bfcb6976 100644 --- a/src/plugins/expressions/README.asciidoc +++ b/src/plugins/expressions/README.asciidoc @@ -46,7 +46,7 @@ image::https://user-images.githubusercontent.com/9773803/74162514-3250a880-4c21- https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionsserversetup.md[Server Setup contract] https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionsserverstart.md[Server Start contract] -===== Browser API +==== Browser API https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicesetup.md[Browser Setup contract] https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsstart.md[Browser Start contract] diff --git a/src/plugins/vis_type_xy/server/plugin.ts b/src/plugins/vis_type_xy/server/plugin.ts index fd670e288ff5b..a9e6020cf3ee8 100644 --- a/src/plugins/vis_type_xy/server/plugin.ts +++ b/src/plugins/vis_type_xy/server/plugin.ts @@ -20,6 +20,7 @@ export const uiSettingsConfig: Record> = { name: i18n.translate('visTypeXy.advancedSettings.visualization.legacyChartsLibrary.name', { defaultMessage: 'Legacy charts library', }), + requiresPageReload: true, value: false, description: i18n.translate( 'visTypeXy.advancedSettings.visualization.legacyChartsLibrary.description', diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md index eb7fbc9d590fa..9c806680f68a2 100644 --- a/x-pack/plugins/event_log/README.md +++ b/x-pack/plugins/event_log/README.md @@ -164,10 +164,12 @@ history records associated with specific saved object ids. ## API +Event Log plugin returns a service instance from setup() and client service from start() methods. + +### Setup ```typescript // IEvent is a TS type generated from the subset of ECS supported -// the NP plugin returns a service instance from setup() and start() export interface IEventLogService { registerProviderActions(provider: string, actions: string[]): void; isProviderActionRegistered(provider: string, action: string): boolean; @@ -237,6 +239,80 @@ properties `start`, `end`, and `duration` in the event. For example: It's anticipated that more "helper" methods like this will be provided in the future. +### Start +```typescript + +export interface IEventLogClientService { + getClient(request: KibanaRequest): IEventLogClient; +} + +export interface IEventLogClient { + findEventsBySavedObjectIds( + type: string, + ids: string[], + options?: Partial + ): Promise; +} +``` + +The plugin exposes an `IEventLogClientService` object to plugins that request it. +These plugins must call `getClient(request)` to get the event log client. + +## Experimental RESTful API + +Usage of the event log allows you to retrieve the events for a given saved object type by the specified set of IDs. +The following API is experimental and can change or be removed in a future release. + +### `GET /api/event_log/{type}/{id}/_find`: Get events for a given saved object type by the ID + +Collects event information from the event log for the selected saved object by type and ID. + +Params: + +|Property|Description|Type| +|---|---|---| +|type|The type of the saved object whose events you're trying to get.|string| +|id|The id of the saved object.|string| + +Query: + +|Property|Description|Type| +|---|---|---| +|page|The page number.|number| +|per_page|The number of events to return per page.|number| +|sort_field|Sorts the response. Could be an event fields returned in the response.|string| +|sort_order|Sort direction, either `asc` or `desc`.|string| +|filter|A KQL string that you filter with an attribute from the event. It should look like `event.action:(execute)`.|string| +|start|The date to start looking for saved object events in the event log. Either an ISO date string, or a duration string that indicates the time since now.|string| +|end|The date to stop looking for saved object events in the event log. Either an ISO date string, or a duration string that indicates the time since now.|string| + +### `POST /api/event_log/{type}/_find`: Retrive events for a given saved object type by the IDs + +Collects event information from the event log for the selected saved object by type and by IDs. + +Params: + +|Property|Description|Type| +|---|---|---| +|type|The type of the saved object whose events you're trying to get.|string| + +Query: + +|Property|Description|Type| +|---|---|---| +|page|The page number.|number| +|per_page|The number of events to return per page.|number| +|sort_field|Sorts the response. Could be an event field returned in the response.|string| +|sort_order|Sort direction, either `asc` or `desc`.|string| +|filter|A KQL string that you filter with an attribute from the event. It should look like `event.action:(execute)`.|string| +|start|The date to start looking for saved object events in the event log. Either an ISO date string, or a duration string that indicates the time since now.|string| +|end|The date to stop looking for saved object events in the event log. Either an ISO date string, or a duration string that indicates the time since now.|string| + +Body: + +|Property|Description|Type| +|---|---|---| +|ids|The array ids of the saved object.|string array| ## Stored data @@ -303,4 +379,3 @@ For more relevant information on ILM, see: [getting started with ILM doc]: https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started-index-lifecycle-management.html [write index alias behavior]: https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-rollover-index.html#indices-rollover-is-write-index - diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index b692d7fe69cd4..c61b431eed46d 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -183,6 +183,13 @@ export const setup = async (arg?: { const enable = (phase: Phases) => createFormToggleAction(`enablePhaseSwitch-${phase}`); + const showDataAllocationOptions = (phase: Phases) => () => { + act(() => { + find(`${phase}-dataTierAllocationControls.dataTierSelect`).simulate('click'); + }); + component.update(); + }; + const createMinAgeActions = (phase: Phases) => { return { hasMinAgeInput: () => exists(`${phase}-selectedMinimumAge`), @@ -377,6 +384,7 @@ export const setup = async (arg?: { }, warm: { enable: enable('warm'), + showDataAllocationOptions: showDataAllocationOptions('warm'), ...createMinAgeActions('warm'), setReplicas: setReplicas('warm'), hasErrorIndicator: () => exists('phaseErrorIndicator-warm'), @@ -388,6 +396,7 @@ export const setup = async (arg?: { }, cold: { enable: enable('cold'), + showDataAllocationOptions: showDataAllocationOptions('cold'), ...createMinAgeActions('cold'), setReplicas: setReplicas('cold'), setFreeze, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index fd211c3de65ae..740aeebb852f1 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -684,43 +684,52 @@ describe('', () => { expect(find('cold-dataTierAllocationControls.dataTierSelect').text()).toContain('Off'); }); }); - }); - - describe('searchable snapshot', () => { describe('on cloud', () => { - describe('new policy', () => { + describe('using legacy data role config', () => { beforeEach(async () => { - // simulate creating a new policy - httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('')]); + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); httpRequestsMockHelpers.setListNodes({ - isUsingDeprecatedDataRoleConfig: false, nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['123'] }, + // On cloud, even if there are data_* roles set, the default, recommended allocation option should not + // be available. + nodesByRoles: { data_hot: ['123'] }, + isUsingDeprecatedDataRoleConfig: true, }); httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); await act(async () => { - testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + testBed = await setup({ + appServicesContext: { + cloud: { + isCloudEnabled: true, + }, + license: licensingMock.createLicense({ license: { type: 'basic' } }), + }, + }); }); const { component } = testBed; component.update(); }); - test('defaults searchable snapshot to true on cloud', async () => { - const { find, actions } = testBed; - await actions.cold.enable(true); - expect( - find('searchableSnapshotField-cold.searchableSnapshotToggle').props()['aria-checked'] - ).toBe(true); + test('removes default, recommended option', async () => { + const { actions, find } = testBed; + await actions.warm.enable(true); + actions.warm.showDataAllocationOptions(); + + expect(find('defaultDataAllocationOption').exists()).toBeFalsy(); + expect(find('customDataAllocationOption').exists()).toBeTruthy(); + expect(find('noneDataAllocationOption').exists()).toBeTruthy(); + // Show the call-to-action for users to migrate their cluster to use node roles + expect(find('cloudDataTierCallout').exists()).toBeTruthy(); }); }); - describe('existing policy', () => { + describe('using node roles', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); httpRequestsMockHelpers.setListNodes({ - isUsingDeprecatedDataRoleConfig: false, nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['123'] }, + nodesByRoles: { data_hot: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, }); httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); @@ -731,19 +740,34 @@ describe('', () => { const { component } = testBed; component.update(); }); - test('correctly sets snapshot repository default to "found-snapshots"', async () => { - const { actions } = testBed; + + test('should show recommended, custom and "off" options on cloud with data roles', async () => { + const { actions, find } = testBed; + + await actions.warm.enable(true); + actions.warm.showDataAllocationOptions(); + expect(find('defaultDataAllocationOption').exists()).toBeTruthy(); + expect(find('customDataAllocationOption').exists()).toBeTruthy(); + expect(find('noneDataAllocationOption').exists()).toBeTruthy(); + // We should not be showing the call-to-action for users to activate the cold tier on cloud + expect(find('cloudMissingColdTierCallout').exists()).toBeFalsy(); + // Do not show the call-to-action for users to migrate their cluster to use node roles + expect(find('cloudDataTierCallout').exists()).toBeFalsy(); + }); + + test('should show cloud notice when cold tier nodes do not exist', async () => { + const { actions, find } = testBed; await actions.cold.enable(true); - await actions.cold.toggleSearchableSnapshot(true); - await actions.savePolicy(); - const latestRequest = server.requests[server.requests.length - 1]; - const request = JSON.parse(JSON.parse(latestRequest.requestBody).body); - expect(request.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual( - 'found-snapshots' - ); + expect(find('cloudMissingColdTierCallout').exists()).toBeTruthy(); + // Assert that other notices are not showing + expect(find('defaultAllocationNotice').exists()).toBeFalsy(); + expect(find('noNodeAttributesWarning').exists()).toBeFalsy(); }); }); }); + }); + + describe('searchable snapshot', () => { describe('on non-enterprise license', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); @@ -780,6 +804,64 @@ describe('', () => { expect(actions.cold.searchableSnapshotDisabledDueToLicense()).toBeTruthy(); }); }); + + describe('on cloud', () => { + describe('new policy', () => { + beforeEach(async () => { + // simulate creating a new policy + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('')]); + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); + + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + + const { component } = testBed; + component.update(); + }); + test('defaults searchable snapshot to true on cloud', async () => { + const { find, actions } = testBed; + await actions.cold.enable(true); + expect( + find('searchableSnapshotField-cold.searchableSnapshotToggle').props()['aria-checked'] + ).toBe(true); + }); + }); + describe('existing policy', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + isUsingDeprecatedDataRoleConfig: false, + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data_hot: ['123'] }, + }); + httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); + + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + + const { component } = testBed; + component.update(); + }); + test('correctly sets snapshot repository default to "found-snapshots"', async () => { + const { actions } = testBed; + await actions.cold.enable(true); + await actions.cold.toggleSearchableSnapshot(true); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const request = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(request.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual( + 'found-snapshots' + ); + }); + }); + }); }); describe('with rollover', () => { beforeEach(async () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts index 113698fdf6df2..b02d190d10899 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts @@ -125,7 +125,7 @@ describe(' node allocation', () => { expect(actions.warm.hasDefaultAllocationWarning()).toBeTruthy(); }); - test('shows default allocation notice when hot tier exists, but not warm tier', async () => { + test('when configuring warm phase shows default allocation notice when hot tier exists, but not warm tier', async () => { httpRequestsMockHelpers.setListNodes({ nodesByAttributes: {}, nodesByRoles: { data_hot: ['test'], data_cold: ['test'] }, @@ -309,7 +309,7 @@ describe(' node allocation', () => { describe('on cloud', () => { describe('with deprecated data role config', () => { - test('should hide data tier option on cloud using legacy node role configuration', async () => { + test('should hide data tier option on cloud', async () => { httpRequestsMockHelpers.setListNodes({ nodesByAttributes: { test: ['123'] }, // On cloud, if using legacy config there will not be any "data_*" roles set. @@ -331,10 +331,29 @@ describe(' node allocation', () => { expect(exists('customDataAllocationOption')).toBeTruthy(); expect(exists('noneDataAllocationOption')).toBeTruthy(); }); + + test('should ask users to migrate to node roles when on cloud using legacy data role', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + // On cloud, if using legacy config there will not be any "data_*" roles set. + nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + const { actions, component, exists } = testBed; + + component.update(); + await actions.warm.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + expect(exists('cloudDataTierCallout')).toBeTruthy(); + }); }); describe('with node role config', () => { - test('shows off, custom and data role options on cloud with data roles', async () => { + test('shows data role, custom and "off" options on cloud with data roles', async () => { httpRequestsMockHelpers.setListNodes({ nodesByAttributes: { test: ['123'] }, nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, @@ -372,7 +391,7 @@ describe(' node allocation', () => { await actions.cold.enable(true); expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(exists('cloudDataTierCallout')).toBeTruthy(); + expect(exists('cloudMissingColdTierCallout')).toBeTruthy(); // Assert that other notices are not showing expect(actions.cold.hasDefaultAllocationNotice()).toBeFalsy(); expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy(); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/cloud_data_tier_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/cloud_data_tier_callout.tsx index 4d3dbbba39037..351d6ac1c530b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/cloud_data_tier_callout.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/cloud_data_tier_callout.tsx @@ -7,21 +7,38 @@ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent } from 'react'; -import { EuiCallOut } from '@elastic/eui'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; const i18nTexts = { title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.title', { - defaultMessage: 'Create a cold tier', + defaultMessage: 'Migrate to data tiers', }), body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.body', { - defaultMessage: 'Edit your Elastic Cloud deployment to set up a cold tier.', + defaultMessage: 'Migrate your Elastic Cloud deployment to use data tiers.', }), + linkText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.linkToCloudDeploymentDescription', + { defaultMessage: 'View cloud deployment' } + ), }; -export const CloudDataTierCallout: FunctionComponent = () => { +interface Props { + linkToCloudDeployment?: string; +} + +/** + * A call-to-action for users to migrate to data tiers if their cluster is still running + * the deprecated node.data:true config. + */ +export const CloudDataTierCallout: FunctionComponent = ({ linkToCloudDeployment }) => { return ( - {i18nTexts.body} + {i18nTexts.body}{' '} + {Boolean(linkToCloudDeployment) && ( + + {i18nTexts.linkText} + + )} ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx index 562267089051a..e43b750849774 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx @@ -11,8 +11,6 @@ import { EuiCallOut } from '@elastic/eui'; import { PhaseWithAllocation, DataTierRole } from '../../../../../../../../../common/types'; -import { AllocationNodeRole } from '../../../../../../../lib'; - const i18nTextsNodeRoleToDataTier: Record = { data_hot: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierHotLabel', { defaultMessage: 'hot', @@ -84,24 +82,13 @@ const i18nTexts = { interface Props { phase: PhaseWithAllocation; - targetNodeRole: AllocationNodeRole; + targetNodeRole: DataTierRole; } export const DefaultAllocationNotice: FunctionComponent = ({ phase, targetNodeRole }) => { - const content = - targetNodeRole === 'none' ? ( - - {i18nTexts.warning[phase].body} - - ) : ( - - {i18nTexts.notice[phase].body(targetNodeRole)} - - ); - - return content; + return ( + + {i18nTexts.notice[phase].body(targetNodeRole)} + + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx new file mode 100644 index 0000000000000..a194f3c07f900 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx @@ -0,0 +1,59 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiCallOut } from '@elastic/eui'; + +import { PhaseWithAllocation } from '../../../../../../../../../common/types'; + +const i18nTexts = { + warning: { + warm: { + title: i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableTitle', + { defaultMessage: 'No nodes assigned to the warm tier' } + ), + body: i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableBody', + { + defaultMessage: + 'Assign at least one node to the warm or hot tier to use role-based allocation. The policy will fail to complete allocation if there are no available nodes.', + } + ), + }, + cold: { + title: i18n.translate( + 'xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle', + { defaultMessage: 'No nodes assigned to the cold tier' } + ), + body: i18n.translate( + 'xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableBody', + { + defaultMessage: + 'Assign at least one node to the cold, warm, or hot tier to use role-based allocation. The policy will fail to complete allocation if there are no available nodes.', + } + ), + }, + }, +}; + +interface Props { + phase: PhaseWithAllocation; +} + +export const DefaultAllocationWarning: FunctionComponent = ({ phase }) => { + return ( + + {i18nTexts.warning[phase].body} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts index b3f57ac24e0d7..938e0a850f933 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts @@ -13,8 +13,12 @@ export { DataTierAllocation } from './data_tier_allocation'; export { DefaultAllocationNotice } from './default_allocation_notice'; +export { DefaultAllocationWarning } from './default_allocation_warning'; + export { NoNodeAttributesWarning } from './no_node_attributes_warning'; +export { MissingColdTierCallout } from './missing_cold_tier_callout'; + export { CloudDataTierCallout } from './cloud_data_tier_callout'; export { LoadingError } from './loading_error'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cold_tier_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cold_tier_callout.tsx new file mode 100644 index 0000000000000..21b8850e0b088 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cold_tier_callout.tsx @@ -0,0 +1,45 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; + +const i18nTexts = { + title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.title', { + defaultMessage: 'Create a cold tier', + }), + body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.body', { + defaultMessage: 'Edit your Elastic Cloud deployment to set up a cold tier.', + }), + linkText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.linkToCloudDeploymentDescription', + { defaultMessage: 'View cloud deployment' } + ), +}; + +interface Props { + linkToCloudDeployment?: string; +} + +/** + * A call-to-action for users to activate their cold tier slider to provision cold tier nodes. + * This may need to be change when we have autoscaling enabled on a cluster because nodes may not + * yet exist, but will automatically be provisioned. + */ +export const MissingColdTierCallout: FunctionComponent = ({ linkToCloudDeployment }) => { + return ( + + {i18nTexts.body}{' '} + {Boolean(linkToCloudDeployment) && ( + + {i18nTexts.linkText} + + )} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx index ad36039728f5c..7a660e0379a8d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx @@ -14,9 +14,7 @@ import { useKibana, useFormData } from '../../../../../../../shared_imports'; import { PhaseWithAllocation } from '../../../../../../../../common/types'; -import { getAvailableNodeRoleForPhase } from '../../../../../../lib/data_tiers'; - -import { isNodeRoleFirstPreference } from '../../../../../../lib'; +import { getAvailableNodeRoleForPhase, isNodeRoleFirstPreference } from '../../../../../../lib'; import { useLoadNodes } from '../../../../../../services/api'; @@ -25,7 +23,9 @@ import { DataTierAllocationType } from '../../../../types'; import { DataTierAllocation, DefaultAllocationNotice, + DefaultAllocationWarning, NoNodeAttributesWarning, + MissingColdTierCallout, CloudDataTierCallout, LoadingError, } from './components'; @@ -65,30 +65,48 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr ); const hasNodeAttrs = Boolean(Object.keys(nodesByAttributes ?? {}).length); const isCloudEnabled = cloud?.isCloudEnabled ?? false; + const cloudDeploymentUrl = cloud?.cloudDeploymentUrl; const renderNotice = () => { switch (allocationType) { case 'node_roles': - if (isCloudEnabled && phase === 'cold') { - const isUsingNodeRolesAllocation = !isUsingDeprecatedDataRoleConfig && hasDataNodeRoles; + /** + * We'll drive Cloud users to add a cold tier to their deployment if there are no nodes with the cold node role. + */ + if (isCloudEnabled && phase === 'cold' && !isUsingDeprecatedDataRoleConfig) { const hasNoNodesWithNodeRole = !nodesByRoles.data_cold?.length; - if (isUsingNodeRolesAllocation && hasNoNodesWithNodeRole) { + if (hasDataNodeRoles && hasNoNodesWithNodeRole) { // Tell cloud users they can deploy nodes on cloud. return ( <> - + ); } } + /** + * Node role allocation moves data in a phase to a corresponding tier of the same name. To prevent policy execution from getting + * stuck ILM allocation will fall back to a previous tier if possible. We show the WARNING below to inform a user when even + * this fallback will not succeed. + */ const allocationNodeRole = getAvailableNodeRoleForPhase(phase, nodesByRoles); - if ( - allocationNodeRole === 'none' || - !isNodeRoleFirstPreference(phase, allocationNodeRole) - ) { + if (allocationNodeRole === 'none') { + return ( + <> + + + + ); + } + + /** + * If we are able to fallback to a data tier that does not map to this phase, we show a notice informing the user that their + * data will not be assigned to a corresponding tier. + */ + if (!isNodeRoleFirstPreference(phase, allocationNodeRole)) { return ( <> @@ -106,6 +124,19 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr ); } + /** + * Special cloud case: when deprecated data role configuration is in use, it means that this deployment is not using + * the new node role based allocation. We drive users to the cloud console to migrate to node role based allocation + * in that case. + */ + if (isCloudEnabled && isUsingDeprecatedDataRoleConfig) { + return ( + <> + + + + ); + } break; default: return null; @@ -141,9 +172,7 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr hasNodeAttributes={hasNodeAttrs} phase={phase} nodes={nodesByAttributes} - disableDataTierOption={Boolean( - isCloudEnabled && !hasDataNodeRoles && isUsingDeprecatedDataRoleConfig - )} + disableDataTierOption={Boolean(isCloudEnabled && isUsingDeprecatedDataRoleConfig)} isLoading={isLoading} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/types.ts b/x-pack/plugins/index_lifecycle_management/public/types.ts index 27b3795c6731f..adfca9ad41b26 100644 --- a/x-pack/plugins/index_lifecycle_management/public/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/types.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { ManagementSetup } from '../../../../src/plugins/management/public'; diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js index 02342c895f077..323d267899bdf 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js @@ -7,8 +7,6 @@ import { cloneDeep, get, pick } from 'lodash'; -import { WEEK } from '../../../../../../../../src/plugins/es_ui_shared/public'; - import { validateId } from './validate_id'; import { validateIndexPattern } from './validate_index_pattern'; import { validateRollupIndex } from './validate_rollup_index'; @@ -66,7 +64,7 @@ export const stepIdToStepConfigMap = { // a few hours as they're being restarted. A delay of 1d would allow them that period to reboot // and the "expense" is pretty negligible in most cases: 1 day of extra non-rolled-up data. rollupDelay: '1d', - cronFrequency: WEEK, + cronFrequency: 'WEEK', fieldToPreferredValueMap: {}, }; diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js index fabd2a0be2454..1c54a42cbee63 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js @@ -181,6 +181,11 @@ describe('Create Rollup Job, step 1: Logistics', () => { expect(options).toEqual(['minute', 'hour', 'day', 'week', 'month', 'year']); }); + it('should default to "WEEK"', () => { + const frequencySelect = find('cronFrequencySelect'); + expect(frequencySelect.props().value).toBe('WEEK'); + }); + describe('every minute', () => { it('should not have any additional configuration', () => { changeFrequency('MINUTE'); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_fields_by_issue_type.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_fields_by_issue_type.tsx index b7a8a45edce5e..03000e8916617 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_fields_by_issue_type.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_fields_by_issue_type.tsx @@ -35,19 +35,20 @@ export const useGetFieldsByIssueType = ({ }: Props): UseGetFieldsByIssueType => { const [isLoading, setIsLoading] = useState(true); const [fields, setFields] = useState({}); + const didCancel = useRef(false); const abortCtrl = useRef(new AbortController()); useEffect(() => { - let didCancel = false; const fetchData = async () => { if (!connector || !issueType) { setIsLoading(false); return; } - abortCtrl.current = new AbortController(); - setIsLoading(true); try { + abortCtrl.current = new AbortController(); + setIsLoading(true); + const res = await getFieldsByIssueType({ http, signal: abortCtrl.current.signal, @@ -55,7 +56,7 @@ export const useGetFieldsByIssueType = ({ id: issueType, }); - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); setFields(res.data ?? {}); if (res.status && res.status === 'error') { @@ -66,22 +67,24 @@ export const useGetFieldsByIssueType = ({ } } } catch (error) { - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); - toastNotifications.addDanger({ - title: i18n.FIELDS_API_ERROR, - text: error.message, - }); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.FIELDS_API_ERROR, + text: error.message, + }); + } } } }; + didCancel.current = false; abortCtrl.current.abort(); fetchData(); return () => { - didCancel = true; - setIsLoading(false); + didCancel.current = true; abortCtrl.current.abort(); }; }, [http, connector, issueType, toastNotifications]); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issue_types.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issue_types.tsx index 4b60a9840c82b..3c35d315a2bcd 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issue_types.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issue_types.tsx @@ -35,27 +35,27 @@ export const useGetIssueTypes = ({ }: Props): UseGetIssueTypes => { const [isLoading, setIsLoading] = useState(true); const [issueTypes, setIssueTypes] = useState([]); + const didCancel = useRef(false); const abortCtrl = useRef(new AbortController()); useEffect(() => { - let didCancel = false; const fetchData = async () => { if (!connector) { setIsLoading(false); return; } - abortCtrl.current = new AbortController(); - setIsLoading(true); - try { + abortCtrl.current = new AbortController(); + setIsLoading(true); + const res = await getIssueTypes({ http, signal: abortCtrl.current.signal, connectorId: connector.id, }); - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); const asOptions = (res.data ?? []).map((type) => ({ text: type.name ?? '', @@ -71,25 +71,29 @@ export const useGetIssueTypes = ({ } } } catch (error) { - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); - toastNotifications.addDanger({ - title: i18n.ISSUE_TYPES_API_ERROR, - text: error.message, - }); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.ISSUE_TYPES_API_ERROR, + text: error.message, + }); + } } } }; + didCancel.current = false; abortCtrl.current.abort(); fetchData(); return () => { - didCancel = true; - setIsLoading(false); + didCancel.current = true; abortCtrl.current.abort(); }; - }, [http, connector, toastNotifications, handleIssueType]); + // handleIssueType unmounts the component at init causing the request to be aborted + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [http, connector, toastNotifications]); return { issueTypes, diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issues.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issues.tsx index 170cf2b53395e..b44b0558f1536 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issues.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issues.tsx @@ -36,20 +36,20 @@ export const useGetIssues = ({ }: Props): UseGetIssues => { const [isLoading, setIsLoading] = useState(false); const [issues, setIssues] = useState([]); + const didCancel = useRef(false); const abortCtrl = useRef(new AbortController()); useEffect(() => { - let didCancel = false; const fetchData = debounce(500, async () => { if (!actionConnector || isEmpty(query)) { setIsLoading(false); return; } - abortCtrl.current = new AbortController(); - setIsLoading(true); - try { + abortCtrl.current = new AbortController(); + setIsLoading(true); + const res = await getIssues({ http, signal: abortCtrl.current.signal, @@ -57,7 +57,7 @@ export const useGetIssues = ({ title: query ?? '', }); - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); setIssues(res.data ?? []); if (res.status && res.status === 'error') { @@ -68,22 +68,24 @@ export const useGetIssues = ({ } } } catch (error) { - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); - toastNotifications.addDanger({ - title: i18n.ISSUES_API_ERROR, - text: error.message, - }); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.ISSUES_API_ERROR, + text: error.message, + }); + } } } }); + didCancel.current = false; abortCtrl.current.abort(); fetchData(); return () => { - didCancel = true; - setIsLoading(false); + didCancel.current = true; abortCtrl.current.abort(); }; }, [http, actionConnector, toastNotifications, query]); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_single_issue.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_single_issue.tsx index 89b42b1a88c1e..6c70286426168 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_single_issue.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_single_issue.tsx @@ -35,10 +35,10 @@ export const useGetSingleIssue = ({ }: Props): UseGetSingleIssue => { const [isLoading, setIsLoading] = useState(false); const [issue, setIssue] = useState(null); + const didCancel = useRef(false); const abortCtrl = useRef(new AbortController()); useEffect(() => { - let didCancel = false; const fetchData = async () => { if (!actionConnector || !id) { setIsLoading(false); @@ -55,7 +55,7 @@ export const useGetSingleIssue = ({ id, }); - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); setIssue(res.data ?? null); if (res.status && res.status === 'error') { @@ -66,22 +66,24 @@ export const useGetSingleIssue = ({ } } } catch (error) { - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); - toastNotifications.addDanger({ - title: i18n.GET_ISSUE_API_ERROR(id), - text: error.message, - }); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.GET_ISSUE_API_ERROR(id), + text: error.message, + }); + } } } }; + didCancel.current = false; abortCtrl.current.abort(); fetchData(); return () => { - didCancel = true; - setIsLoading(false); + didCancel.current = true; abortCtrl.current.abort(); }; }, [http, actionConnector, id, toastNotifications]); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_incident_types.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_incident_types.tsx index 99964f466058f..34cbb0a69b0f4 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_incident_types.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_incident_types.tsx @@ -34,27 +34,27 @@ export const useGetIncidentTypes = ({ }: Props): UseGetIncidentTypes => { const [isLoading, setIsLoading] = useState(true); const [incidentTypes, setIncidentTypes] = useState([]); + const didCancel = useRef(false); const abortCtrl = useRef(new AbortController()); useEffect(() => { - let didCancel = false; const fetchData = async () => { if (!connector) { setIsLoading(false); return; } - abortCtrl.current = new AbortController(); - setIsLoading(true); - try { + abortCtrl.current = new AbortController(); + setIsLoading(true); + const res = await getIncidentTypes({ http, signal: abortCtrl.current.signal, connectorId: connector.id, }); - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); setIncidentTypes(res.data ?? []); if (res.status && res.status === 'error') { @@ -65,22 +65,24 @@ export const useGetIncidentTypes = ({ } } } catch (error) { - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); - toastNotifications.addDanger({ - title: i18n.INCIDENT_TYPES_API_ERROR, - text: error.message, - }); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.INCIDENT_TYPES_API_ERROR, + text: error.message, + }); + } } } }; + didCancel.current = false; abortCtrl.current.abort(); fetchData(); return () => { - didCancel = true; - setIsLoading(false); + didCancel.current = true; abortCtrl.current.abort(); }; }, [http, connector, toastNotifications]); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_severity.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_severity.tsx index 0a71891ae41b2..5b44c6b4a32b2 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_severity.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_severity.tsx @@ -7,9 +7,9 @@ import { useState, useEffect, useRef } from 'react'; import { HttpSetup, ToastsApi } from 'kibana/public'; +import { ActionConnector } from '../../../containers/types'; import { getSeverity } from './api'; import * as i18n from './translations'; -import { ActionConnector } from '../../../containers/types'; type Severity = Array<{ id: number; name: string }>; @@ -31,26 +31,26 @@ export const useGetSeverity = ({ http, toastNotifications, connector }: Props): const [isLoading, setIsLoading] = useState(true); const [severity, setSeverity] = useState([]); const abortCtrl = useRef(new AbortController()); + const didCancel = useRef(false); useEffect(() => { - let didCancel = false; const fetchData = async () => { if (!connector) { setIsLoading(false); return; } - abortCtrl.current = new AbortController(); - setIsLoading(true); - try { + abortCtrl.current = new AbortController(); + setIsLoading(true); + const res = await getSeverity({ http, signal: abortCtrl.current.signal, connectorId: connector.id, }); - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); setSeverity(res.data ?? []); @@ -62,22 +62,24 @@ export const useGetSeverity = ({ http, toastNotifications, connector }: Props): } } } catch (error) { - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); - toastNotifications.addDanger({ - title: i18n.SEVERITY_API_ERROR, - text: error.message, - }); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.SEVERITY_API_ERROR, + text: error.message, + }); + } } } }; + didCancel.current = false; abortCtrl.current.abort(); fetchData(); return () => { - didCancel = true; - setIsLoading(false); + didCancel.current = true; abortCtrl.current.abort(); }; }, [http, connector, toastNotifications]); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.tsx index 16e905bdabfee..a979f96d84ab2 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.tsx @@ -37,20 +37,20 @@ export const useGetChoices = ({ }: UseGetChoicesProps): UseGetChoices => { const [isLoading, setIsLoading] = useState(false); const [choices, setChoices] = useState([]); + const didCancel = useRef(false); const abortCtrl = useRef(new AbortController()); useEffect(() => { - let didCancel = false; const fetchData = async () => { if (!connector) { setIsLoading(false); return; } - abortCtrl.current = new AbortController(); - setIsLoading(true); - try { + abortCtrl.current = new AbortController(); + setIsLoading(true); + const res = await getChoices({ http, signal: abortCtrl.current.signal, @@ -58,7 +58,7 @@ export const useGetChoices = ({ fields, }); - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); setChoices(res.data ?? []); if (res.status && res.status === 'error') { @@ -71,22 +71,24 @@ export const useGetChoices = ({ } } } catch (error) { - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); - toastNotifications.addDanger({ - title: i18n.CHOICES_API_ERROR, - text: error.message, - }); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.CHOICES_API_ERROR, + text: error.message, + }); + } } } }; + didCancel.current = false; abortCtrl.current.abort(); fetchData(); return () => { - didCancel = true; - setIsLoading(false); + didCancel.current = true; abortCtrl.current.abort(); }; // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/use_action_types.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_action_types.tsx index ff5762b8476de..3590fffdef5b2 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/use_action_types.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/use_action_types.tsx @@ -22,25 +22,25 @@ export const useActionTypes = (): UseActionTypesResponse => { const [, dispatchToaster] = useStateToaster(); const [loading, setLoading] = useState(true); const [actionTypes, setActionTypes] = useState([]); - const didCancel = useRef(false); - const abortCtrl = useRef(new AbortController()); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); const queryFirstTime = useRef(true); const refetchActionTypes = useCallback(async () => { try { setLoading(true); - didCancel.current = false; - abortCtrl.current.abort(); - abortCtrl.current = new AbortController(); + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); - const res = await fetchActionTypes({ signal: abortCtrl.current.signal }); + const res = await fetchActionTypes({ signal: abortCtrlRef.current.signal }); - if (!didCancel.current) { + if (!isCancelledRef.current) { setLoading(false); setActionTypes(res); } } catch (error) { - if (!didCancel.current) { + if (!isCancelledRef.current) { setLoading(false); setActionTypes([]); errorToToaster({ @@ -59,8 +59,8 @@ export const useActionTypes = (): UseActionTypesResponse => { } return () => { - didCancel.current = true; - abortCtrl.current.abort(); + isCancelledRef.current = true; + abortCtrlRef.current.abort(); queryFirstTime.current = true; }; }, [refetchActionTypes]); diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx index cc8c93fc990eb..21d1832796ba8 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { useEffect, useCallback, useReducer } from 'react'; +import { useEffect, useCallback, useReducer, useRef } from 'react'; import { getCaseConfigure, patchCaseConfigure, postCaseConfigure } from './api'; import { @@ -207,129 +207,128 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { }, []); const [, dispatchToaster] = useStateToaster(); + const isCancelledRefetchRef = useRef(false); + const abortCtrlRefetchRef = useRef(new AbortController()); - const refetchCaseConfigure = useCallback(() => { - let didCancel = false; - const abortCtrl = new AbortController(); + const isCancelledPersistRef = useRef(false); + const abortCtrlPersistRef = useRef(new AbortController()); - const fetchCaseConfiguration = async () => { - try { - setLoading(true); - const res = await getCaseConfigure({ signal: abortCtrl.signal }); - if (!didCancel) { - if (res != null) { - setConnector(res.connector); - if (setClosureType != null) { - setClosureType(res.closureType); - } - setVersion(res.version); - setMappings(res.mappings); + const refetchCaseConfigure = useCallback(async () => { + try { + isCancelledRefetchRef.current = false; + abortCtrlRefetchRef.current.abort(); + abortCtrlRefetchRef.current = new AbortController(); - if (!state.firstLoad) { - setFirstLoad(true); - if (setCurrentConfiguration != null) { - setCurrentConfiguration({ - closureType: res.closureType, - connector: { - ...res.connector, - }, - }); - } - } - if (res.error != null) { - errorToToaster({ - dispatchToaster, - error: new Error(res.error), - title: i18n.ERROR_TITLE, + setLoading(true); + const res = await getCaseConfigure({ signal: abortCtrlRefetchRef.current.signal }); + + if (!isCancelledRefetchRef.current) { + if (res != null) { + setConnector(res.connector); + if (setClosureType != null) { + setClosureType(res.closureType); + } + setVersion(res.version); + setMappings(res.mappings); + + if (!state.firstLoad) { + setFirstLoad(true); + if (setCurrentConfiguration != null) { + setCurrentConfiguration({ + closureType: res.closureType, + connector: { + ...res.connector, + }, }); } } - setLoading(false); + if (res.error != null) { + errorToToaster({ + dispatchToaster, + error: new Error(res.error), + title: i18n.ERROR_TITLE, + }); + } } - } catch (error) { - if (!didCancel) { - setLoading(false); + setLoading(false); + } + } catch (error) { + if (!isCancelledRefetchRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ dispatchToaster, error: error.body && error.body.message ? new Error(error.body.message) : error, title: i18n.ERROR_TITLE, }); } + setLoading(false); } - }; - - fetchCaseConfiguration(); - - return () => { - didCancel = true; - abortCtrl.abort(); - }; + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.firstLoad]); const persistCaseConfigure = useCallback( async ({ connector, closureType }: ConnectorConfiguration) => { - let didCancel = false; - const abortCtrl = new AbortController(); - const saveCaseConfiguration = async () => { - try { - setPersistLoading(true); - const connectorObj = { - connector, - closure_type: closureType, - }; - const res = - state.version.length === 0 - ? await postCaseConfigure(connectorObj, abortCtrl.signal) - : await patchCaseConfigure( - { - ...connectorObj, - version: state.version, - }, - abortCtrl.signal - ); - if (!didCancel) { - setConnector(res.connector); - if (setClosureType) { - setClosureType(res.closureType); - } - setVersion(res.version); - setMappings(res.mappings); - if (setCurrentConfiguration != null) { - setCurrentConfiguration({ - closureType: res.closureType, - connector: { - ...res.connector, + try { + isCancelledPersistRef.current = false; + abortCtrlPersistRef.current.abort(); + abortCtrlPersistRef.current = new AbortController(); + setPersistLoading(true); + + const connectorObj = { + connector, + closure_type: closureType, + }; + + const res = + state.version.length === 0 + ? await postCaseConfigure(connectorObj, abortCtrlPersistRef.current.signal) + : await patchCaseConfigure( + { + ...connectorObj, + version: state.version, }, - }); - } - if (res.error != null) { - errorToToaster({ - dispatchToaster, - error: new Error(res.error), - title: i18n.ERROR_TITLE, - }); - } - displaySuccessToast(i18n.SUCCESS_CONFIGURE, dispatchToaster); - setPersistLoading(false); + abortCtrlPersistRef.current.signal + ); + + if (!isCancelledPersistRef.current) { + setConnector(res.connector); + if (setClosureType) { + setClosureType(res.closureType); + } + setVersion(res.version); + setMappings(res.mappings); + if (setCurrentConfiguration != null) { + setCurrentConfiguration({ + closureType: res.closureType, + connector: { + ...res.connector, + }, + }); } - } catch (error) { - if (!didCancel) { - setConnector(state.currentConfiguration.connector); - setPersistLoading(false); + if (res.error != null) { + errorToToaster({ + dispatchToaster, + error: new Error(res.error), + title: i18n.ERROR_TITLE, + }); + } + displaySuccessToast(i18n.SUCCESS_CONFIGURE, dispatchToaster); + setPersistLoading(false); + } + } catch (error) { + if (!isCancelledPersistRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); } + setConnector(state.currentConfiguration.connector); + setPersistLoading(false); } - }; - saveCaseConfiguration(); - return () => { - didCancel = true; - abortCtrl.abort(); - }; + } }, [ dispatchToaster, @@ -345,6 +344,12 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { useEffect(() => { refetchCaseConfigure(); + return () => { + isCancelledRefetchRef.current = true; + abortCtrlRefetchRef.current.abort(); + isCancelledPersistRef.current = true; + abortCtrlPersistRef.current.abort(); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/use_connectors.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_connectors.tsx index d21e50902ca83..338d04f702c63 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/use_connectors.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/use_connectors.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { useStateToaster, errorToToaster } from '../../../common/components/toasters'; import * as i18n from '../translations'; @@ -22,40 +22,45 @@ export const useConnectors = (): UseConnectorsResponse => { const [, dispatchToaster] = useStateToaster(); const [loading, setLoading] = useState(true); const [connectors, setConnectors] = useState([]); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); - const refetchConnectors = useCallback(() => { - let didCancel = false; - const abortCtrl = new AbortController(); - const getConnectors = async () => { - try { - setLoading(true); - const res = await fetchConnectors({ signal: abortCtrl.signal }); - if (!didCancel) { - setLoading(false); - setConnectors(res); - } - } catch (error) { - if (!didCancel) { - setLoading(false); - setConnectors([]); + const refetchConnectors = useCallback(async () => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + + setLoading(true); + const res = await fetchConnectors({ signal: abortCtrlRef.current.signal }); + + if (!isCancelledRef.current) { + setLoading(false); + setConnectors(res); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); } + + setLoading(false); + setConnectors([]); } - }; - getConnectors(); - return () => { - didCancel = true; - abortCtrl.abort(); - }; + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { refetchConnectors(); + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx index 0fe45aaab799b..da069ee6f1075 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback, useReducer } from 'react'; +import { useCallback, useReducer, useRef, useEffect } from 'react'; import { CaseStatuses } from '../../../../case/common/api'; import { displaySuccessToast, @@ -87,49 +87,45 @@ export const useUpdateCases = (): UseUpdateCases => { isUpdated: false, }); const [, dispatchToaster] = useStateToaster(); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); - const dispatchUpdateCases = useCallback((cases: BulkUpdateStatus[], action: string) => { - let cancel = false; - const abortCtrl = new AbortController(); + const dispatchUpdateCases = useCallback(async (cases: BulkUpdateStatus[], action: string) => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); - const patchData = async () => { - try { - dispatch({ type: 'FETCH_INIT' }); - const patchResponse = await patchCasesStatus(cases, abortCtrl.signal); - if (!cancel) { - const resultCount = Object.keys(patchResponse).length; - const firstTitle = patchResponse[0].title; + dispatch({ type: 'FETCH_INIT' }); + const patchResponse = await patchCasesStatus(cases, abortCtrlRef.current.signal); - dispatch({ type: 'FETCH_SUCCESS', payload: true }); + if (!isCancelledRef.current) { + const resultCount = Object.keys(patchResponse).length; + const firstTitle = patchResponse[0].title; - const messageArgs = { - totalCases: resultCount, - caseTitle: resultCount === 1 ? firstTitle : '', - }; + dispatch({ type: 'FETCH_SUCCESS', payload: true }); + const messageArgs = { + totalCases: resultCount, + caseTitle: resultCount === 1 ? firstTitle : '', + }; - const message = - action === 'status' - ? getStatusToasterMessage(patchResponse[0].status, messageArgs) - : ''; + const message = + action === 'status' ? getStatusToasterMessage(patchResponse[0].status, messageArgs) : ''; - displaySuccessToast(message, dispatchToaster); - } - } catch (error) { - if (!cancel) { + displaySuccessToast(message, dispatchToaster); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: 'FETCH_FAILURE' }); } + dispatch({ type: 'FETCH_FAILURE' }); } - }; - patchData(); - return () => { - cancel = true; - abortCtrl.abort(); - }; + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -137,14 +133,25 @@ export const useUpdateCases = (): UseUpdateCases => { dispatch({ type: 'RESET_IS_UPDATED' }); }, []); - const updateBulkStatus = useCallback((cases: Case[], status: string) => { - const updateCasesStatus: BulkUpdateStatus[] = cases.map((theCase) => ({ - status, - id: theCase.id, - version: theCase.version, - })); - dispatchUpdateCases(updateCasesStatus, 'status'); + const updateBulkStatus = useCallback( + (cases: Case[], status: string) => { + const updateCasesStatus: BulkUpdateStatus[] = cases.map((theCase) => ({ + status, + id: theCase.id, + version: theCase.version, + })); + dispatchUpdateCases(updateCasesStatus, 'status'); + }, // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + useEffect(() => { + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; }, []); + return { ...state, updateBulkStatus, dispatchResetIsUpdated }; }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx index 923c20dcf8ebd..f3d59a2883f2a 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback, useReducer } from 'react'; +import { useCallback, useReducer, useRef, useEffect } from 'react'; import { displaySuccessToast, errorToToaster, @@ -78,45 +78,43 @@ export const useDeleteCases = (): UseDeleteCase => { isDeleted: false, }); const [, dispatchToaster] = useStateToaster(); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); - const dispatchDeleteCases = useCallback((cases: DeleteCase[]) => { - let cancel = false; - const abortCtrl = new AbortController(); + const dispatchDeleteCases = useCallback(async (cases: DeleteCase[]) => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + dispatch({ type: 'FETCH_INIT' }); - const deleteData = async () => { - try { - dispatch({ type: 'FETCH_INIT' }); - const caseIds = cases.map((theCase) => theCase.id); - // We don't allow user batch delete sub cases on UI at the moment. - if (cases[0].type != null || cases.length > 1) { - await deleteCases(caseIds, abortCtrl.signal); - } else { - await deleteSubCases(caseIds, abortCtrl.signal); - } + const caseIds = cases.map((theCase) => theCase.id); + // We don't allow user batch delete sub cases on UI at the moment. + if (cases[0].type != null || cases.length > 1) { + await deleteCases(caseIds, abortCtrlRef.current.signal); + } else { + await deleteSubCases(caseIds, abortCtrlRef.current.signal); + } - if (!cancel) { - dispatch({ type: 'FETCH_SUCCESS', payload: true }); - displaySuccessToast( - i18n.DELETED_CASES(cases.length, cases.length === 1 ? cases[0].title : ''), - dispatchToaster - ); - } - } catch (error) { - if (!cancel) { + if (!isCancelledRef.current) { + dispatch({ type: 'FETCH_SUCCESS', payload: true }); + displaySuccessToast( + i18n.DELETED_CASES(cases.length, cases.length === 1 ? cases[0].title : ''), + dispatchToaster + ); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_DELETING, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: 'FETCH_FAILURE' }); } + dispatch({ type: 'FETCH_FAILURE' }); } - }; - deleteData(); - return () => { - abortCtrl.abort(); - cancel = true; - }; + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -142,5 +140,12 @@ export const useDeleteCases = (): UseDeleteCase => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.isDisplayConfirmDeleteModal]); + useEffect(() => { + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; + }, []); + return { ...state, dispatchResetIsDeleted, handleOnDeleteConfirm, handleToggleModal }; }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.tsx index 9b536f32e7eb8..9b10247794c8d 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState, useRef } from 'react'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { getActionLicense } from './api'; @@ -28,53 +28,58 @@ const MINIMUM_LICENSE_REQUIRED_CONNECTOR = '.jira'; export const useGetActionLicense = (): ActionLicenseState => { const [actionLicenseState, setActionLicensesState] = useState(initialData); - const [, dispatchToaster] = useStateToaster(); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); - const fetchActionLicense = useCallback(() => { - let didCancel = false; - const abortCtrl = new AbortController(); - const fetchData = async () => { + const fetchActionLicense = useCallback(async () => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); setActionLicensesState({ - ...actionLicenseState, + ...initialData, isLoading: true, }); - try { - const response = await getActionLicense(abortCtrl.signal); - if (!didCancel) { - setActionLicensesState({ - actionLicense: - response.find((l) => l.id === MINIMUM_LICENSE_REQUIRED_CONNECTOR) ?? null, - isLoading: false, - isError: false, - }); - } - } catch (error) { - if (!didCancel) { + + const response = await getActionLicense(abortCtrlRef.current.signal); + + if (!isCancelledRef.current) { + setActionLicensesState({ + actionLicense: response.find((l) => l.id === MINIMUM_LICENSE_REQUIRED_CONNECTOR) ?? null, + isLoading: false, + isError: false, + }); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - setActionLicensesState({ - actionLicense: null, - isLoading: false, - isError: true, - }); } + + setActionLicensesState({ + actionLicense: null, + isLoading: false, + isError: true, + }); } - }; - fetchData(); - return () => { - didCancel = true; - abortCtrl.abort(); - }; + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [actionLicenseState]); useEffect(() => { fetchActionLicense(); + + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + return { ...actionLicenseState }; }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx index 1c4476e3cb2b7..fb8da8d0663ee 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { isEmpty } from 'lodash'; import { useEffect, useReducer, useCallback, useRef } from 'react'; import { CaseStatuses, CaseType } from '../../../../case/common/api'; @@ -96,48 +95,48 @@ export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => { data: initialData, }); const [, dispatchToaster] = useStateToaster(); - const abortCtrl = useRef(new AbortController()); - const didCancel = useRef(false); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); const updateCase = useCallback((newCase: Case) => { dispatch({ type: 'UPDATE_CASE', payload: newCase }); }, []); const callFetch = useCallback(async () => { - const fetchData = async () => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); dispatch({ type: 'FETCH_INIT' }); - try { - const response = await (subCaseId - ? getSubCase(caseId, subCaseId, true, abortCtrl.current.signal) - : getCase(caseId, true, abortCtrl.current.signal)); - if (!didCancel.current) { - dispatch({ type: 'FETCH_SUCCESS', payload: response }); - } - } catch (error) { - if (!didCancel.current) { + + const response = await (subCaseId + ? getSubCase(caseId, subCaseId, true, abortCtrlRef.current.signal) + : getCase(caseId, true, abortCtrlRef.current.signal)); + + if (!isCancelledRef.current) { + dispatch({ type: 'FETCH_SUCCESS', payload: response }); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: 'FETCH_FAILURE' }); } + dispatch({ type: 'FETCH_FAILURE' }); } - }; - didCancel.current = false; - abortCtrl.current.abort(); - abortCtrl.current = new AbortController(); - fetchData(); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [caseId, subCaseId]); useEffect(() => { - if (!isEmpty(caseId)) { - callFetch(); - } + callFetch(); + return () => { - didCancel.current = true; - abortCtrl.current.abort(); + isCancelledRef.current = true; + abortCtrlRef.current.abort(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [caseId, subCaseId]); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx index 12e5f6643351f..cc8deaf72eef6 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx @@ -6,7 +6,7 @@ */ import { isEmpty, uniqBy } from 'lodash/fp'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useState, useRef } from 'react'; import deepEqual from 'fast-deep-equal'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; @@ -244,64 +244,67 @@ export const useGetCaseUserActions = ( const [caseUserActionsState, setCaseUserActionsState] = useState( initialData ); - const abortCtrl = useRef(new AbortController()); - const didCancel = useRef(false); + const abortCtrlRef = useRef(new AbortController()); + const isCancelledRef = useRef(false); const [, dispatchToaster] = useStateToaster(); const fetchCaseUserActions = useCallback( - (thisCaseId: string, thisSubCaseId?: string) => { - const fetchData = async () => { - try { + async (thisCaseId: string, thisSubCaseId?: string) => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + setCaseUserActionsState({ + ...caseUserActionsState, + isLoading: true, + }); + + const response = await (thisSubCaseId + ? getSubCaseUserActions(thisCaseId, thisSubCaseId, abortCtrlRef.current.signal) + : getCaseUserActions(thisCaseId, abortCtrlRef.current.signal)); + + if (!isCancelledRef.current) { + // Attention Future developer + // We are removing the first item because it will always be the creation of the case + // and we do not want it to simplify our life + const participants = !isEmpty(response) + ? uniqBy('actionBy.username', response).map((cau) => cau.actionBy) + : []; + + const caseUserActions = !isEmpty(response) + ? thisSubCaseId + ? response + : response.slice(1) + : []; + setCaseUserActionsState({ - ...caseUserActionsState, - isLoading: true, + caseUserActions, + ...getPushedInfo(caseUserActions, caseConnectorId), + isLoading: false, + isError: false, + participants, }); - - const response = await (thisSubCaseId - ? getSubCaseUserActions(thisCaseId, thisSubCaseId, abortCtrl.current.signal) - : getCaseUserActions(thisCaseId, abortCtrl.current.signal)); - if (!didCancel.current) { - // Attention Future developer - // We are removing the first item because it will always be the creation of the case - // and we do not want it to simplify our life - const participants = !isEmpty(response) - ? uniqBy('actionBy.username', response).map((cau) => cau.actionBy) - : []; - - const caseUserActions = !isEmpty(response) - ? thisSubCaseId - ? response - : response.slice(1) - : []; - setCaseUserActionsState({ - caseUserActions, - ...getPushedInfo(caseUserActions, caseConnectorId), - isLoading: false, - isError: false, - participants, - }); - } - } catch (error) { - if (!didCancel.current) { + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - setCaseUserActionsState({ - caseServices: {}, - caseUserActions: [], - hasDataToPush: false, - isError: true, - isLoading: false, - participants: [], - }); } + + setCaseUserActionsState({ + caseServices: {}, + caseUserActions: [], + hasDataToPush: false, + isError: true, + isLoading: false, + participants: [], + }); } - }; - abortCtrl.current.abort(); - abortCtrl.current = new AbortController(); - fetchData(); + } }, // eslint-disable-next-line react-hooks/exhaustive-deps [caseConnectorId] @@ -313,8 +316,8 @@ export const useGetCaseUserActions = ( } return () => { - didCancel.current = true; - abortCtrl.current.abort(); + isCancelledRef.current = true; + abortCtrlRef.current.abort(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [caseId, subCaseId]); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx index 298d817fffa88..c83cc02dedb97 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback, useEffect, useReducer } from 'react'; +import { useCallback, useEffect, useReducer, useRef } from 'react'; import { CaseStatuses } from '../../../../case/common/api'; import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from './constants'; import { AllCases, SortFieldCase, FilterOptions, QueryParams, Case, UpdateByKey } from './types'; @@ -139,6 +139,10 @@ export const useGetCases = (initialQueryParams?: QueryParams): UseGetCases => { selectedCases: [], }); const [, dispatchToaster] = useStateToaster(); + const didCancelFetchCases = useRef(false); + const didCancelUpdateCases = useRef(false); + const abortCtrlFetchCases = useRef(new AbortController()); + const abortCtrlUpdateCases = useRef(new AbortController()); const setSelectedCases = useCallback((mySelectedCases: Case[]) => { dispatch({ type: 'UPDATE_TABLE_SELECTIONS', payload: mySelectedCases }); @@ -152,81 +156,69 @@ export const useGetCases = (initialQueryParams?: QueryParams): UseGetCases => { dispatch({ type: 'UPDATE_FILTER_OPTIONS', payload: newFilters }); }, []); - const fetchCases = useCallback((filterOptions: FilterOptions, queryParams: QueryParams) => { - let didCancel = false; - const abortCtrl = new AbortController(); - - const fetchData = async () => { + const fetchCases = useCallback(async (filterOptions: FilterOptions, queryParams: QueryParams) => { + try { + didCancelFetchCases.current = false; + abortCtrlFetchCases.current.abort(); + abortCtrlFetchCases.current = new AbortController(); dispatch({ type: 'FETCH_INIT', payload: 'cases' }); - try { - const response = await getCases({ - filterOptions, - queryParams, - signal: abortCtrl.signal, + + const response = await getCases({ + filterOptions, + queryParams, + signal: abortCtrlFetchCases.current.signal, + }); + + if (!didCancelFetchCases.current) { + dispatch({ + type: 'FETCH_CASES_SUCCESS', + payload: response, }); - if (!didCancel) { - dispatch({ - type: 'FETCH_CASES_SUCCESS', - payload: response, - }); - } - } catch (error) { - if (!didCancel) { + } + } catch (error) { + if (!didCancelFetchCases.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: 'FETCH_FAILURE', payload: 'cases' }); } + dispatch({ type: 'FETCH_FAILURE', payload: 'cases' }); } - }; - fetchData(); - return () => { - abortCtrl.abort(); - didCancel = true; - }; + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => fetchCases(state.filterOptions, state.queryParams), [ - state.queryParams, - state.filterOptions, - ]); - const dispatchUpdateCaseProperty = useCallback( - ({ updateKey, updateValue, caseId, refetchCasesStatus, version }: UpdateCase) => { - let didCancel = false; - const abortCtrl = new AbortController(); - - const fetchData = async () => { + async ({ updateKey, updateValue, caseId, refetchCasesStatus, version }: UpdateCase) => { + try { + didCancelUpdateCases.current = false; + abortCtrlUpdateCases.current.abort(); + abortCtrlUpdateCases.current = new AbortController(); dispatch({ type: 'FETCH_INIT', payload: 'caseUpdate' }); - try { - await patchCase( - caseId, - { [updateKey]: updateValue }, - // saved object versions are typed as string | undefined, hope that's not true - version ?? '', - abortCtrl.signal - ); - if (!didCancel) { - dispatch({ type: 'FETCH_UPDATE_CASE_SUCCESS' }); - fetchCases(state.filterOptions, state.queryParams); - refetchCasesStatus(); - } - } catch (error) { - if (!didCancel) { + + await patchCase( + caseId, + { [updateKey]: updateValue }, + // saved object versions are typed as string | undefined, hope that's not true + version ?? '', + abortCtrlUpdateCases.current.signal + ); + + if (!didCancelUpdateCases.current) { + dispatch({ type: 'FETCH_UPDATE_CASE_SUCCESS' }); + fetchCases(state.filterOptions, state.queryParams); + refetchCasesStatus(); + } + } catch (error) { + if (!didCancelUpdateCases.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); - dispatch({ type: 'FETCH_FAILURE', payload: 'caseUpdate' }); } + dispatch({ type: 'FETCH_FAILURE', payload: 'caseUpdate' }); } - }; - fetchData(); - return () => { - abortCtrl.abort(); - didCancel = true; - }; + } }, // eslint-disable-next-line react-hooks/exhaustive-deps [state.filterOptions, state.queryParams] @@ -237,6 +229,17 @@ export const useGetCases = (initialQueryParams?: QueryParams): UseGetCases => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.filterOptions, state.queryParams]); + useEffect(() => { + fetchCases(state.filterOptions, state.queryParams); + return () => { + didCancelFetchCases.current = true; + didCancelUpdateCases.current = true; + abortCtrlFetchCases.current.abort(); + abortCtrlUpdateCases.current.abort(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [state.queryParams, state.filterOptions]); + return { ...state, dispatchUpdateCaseProperty, diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases_status.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases_status.tsx index 057fc05008bb0..087f7ef455cba 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases_status.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases_status.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState, useRef } from 'react'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { getCasesStatus } from './api'; @@ -32,51 +32,56 @@ export interface UseGetCasesStatus extends CasesStatusState { export const useGetCasesStatus = (): UseGetCasesStatus => { const [casesStatusState, setCasesStatusState] = useState(initialData); const [, dispatchToaster] = useStateToaster(); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); - const fetchCasesStatus = useCallback(() => { - let didCancel = false; - const abortCtrl = new AbortController(); - const fetchData = async () => { + const fetchCasesStatus = useCallback(async () => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); setCasesStatusState({ - ...casesStatusState, + ...initialData, isLoading: true, }); - try { - const response = await getCasesStatus(abortCtrl.signal); - if (!didCancel) { - setCasesStatusState({ - ...response, - isLoading: false, - isError: false, - }); - } - } catch (error) { - if (!didCancel) { + + const response = await getCasesStatus(abortCtrlRef.current.signal); + + if (!isCancelledRef.current) { + setCasesStatusState({ + ...response, + isLoading: false, + isError: false, + }); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - setCasesStatusState({ - countClosedCases: 0, - countInProgressCases: 0, - countOpenCases: 0, - isLoading: false, - isError: true, - }); } + setCasesStatusState({ + countClosedCases: 0, + countInProgressCases: 0, + countOpenCases: 0, + isLoading: false, + isError: true, + }); } - }; - fetchData(); - return () => { - didCancel = true; - abortCtrl.abort(); - }; + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [casesStatusState]); + }, []); useEffect(() => { fetchCasesStatus(); + + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_reporters.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_reporters.tsx index 25c483045b84f..f2c33ec4730fe 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_reporters.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_reporters.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import { useCallback, useEffect, useState } from 'react'; - +import { useCallback, useEffect, useState, useRef } from 'react'; import { isEmpty } from 'lodash/fp'; + import { User } from '../../../../case/common/api'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { getReporters } from './api'; @@ -35,57 +35,61 @@ export const useGetReporters = (): UseGetReporters => { const [reportersState, setReporterState] = useState(initialData); const [, dispatchToaster] = useStateToaster(); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); - const fetchReporters = useCallback(() => { - let didCancel = false; - const abortCtrl = new AbortController(); - const fetchData = async () => { + const fetchReporters = useCallback(async () => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); setReporterState({ ...reportersState, isLoading: true, }); - try { - const response = await getReporters(abortCtrl.signal); - const myReporters = response - .map((r) => - r.full_name == null || isEmpty(r.full_name) ? r.username ?? '' : r.full_name - ) - .filter((u) => !isEmpty(u)); - if (!didCancel) { - setReporterState({ - reporters: myReporters, - respReporters: response, - isLoading: false, - isError: false, - }); - } - } catch (error) { - if (!didCancel) { + + const response = await getReporters(abortCtrlRef.current.signal); + const myReporters = response + .map((r) => (r.full_name == null || isEmpty(r.full_name) ? r.username ?? '' : r.full_name)) + .filter((u) => !isEmpty(u)); + + if (!isCancelledRef.current) { + setReporterState({ + reporters: myReporters, + respReporters: response, + isLoading: false, + isError: false, + }); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - setReporterState({ - reporters: [], - respReporters: [], - isLoading: false, - isError: true, - }); } + + setReporterState({ + reporters: [], + respReporters: [], + isLoading: false, + isError: true, + }); } - }; - fetchData(); - return () => { - didCancel = true; - abortCtrl.abort(); - }; + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [reportersState]); useEffect(() => { fetchReporters(); + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + return { ...reportersState, fetchReporters }; }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_tags.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_tags.tsx index 208516d302eb4..4a7a298e2cd86 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_tags.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_tags.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import { useEffect, useReducer } from 'react'; - +import { useEffect, useReducer, useRef, useCallback } from 'react'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { getTags } from './api'; import * as i18n from './translations'; @@ -59,37 +58,42 @@ export const useGetTags = (): UseGetTags => { tags: initialData, }); const [, dispatchToaster] = useStateToaster(); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); - const callFetch = () => { - let didCancel = false; - const abortCtrl = new AbortController(); - - const fetchData = async () => { + const callFetch = useCallback(async () => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); dispatch({ type: 'FETCH_INIT' }); - try { - const response = await getTags(abortCtrl.signal); - if (!didCancel) { - dispatch({ type: 'FETCH_SUCCESS', payload: response }); - } - } catch (error) { - if (!didCancel) { + + const response = await getTags(abortCtrlRef.current.signal); + + if (!isCancelledRef.current) { + dispatch({ type: 'FETCH_SUCCESS', payload: response }); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: 'FETCH_FAILURE' }); } + dispatch({ type: 'FETCH_FAILURE' }); } - }; - fetchData(); - return () => { - abortCtrl.abort(); - didCancel = true; - }; - }; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useEffect(() => { callFetch(); + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return { ...state, fetchTags: callFetch }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_case.tsx index c4fa030473534..d890c050f5034 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_case.tsx @@ -6,7 +6,6 @@ */ import { useReducer, useCallback, useRef, useEffect } from 'react'; - import { CasePostRequest } from '../../../../case/common/api'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { postCase } from './api'; @@ -51,38 +50,41 @@ export const usePostCase = (): UsePostCase => { isError: false, }); const [, dispatchToaster] = useStateToaster(); - const cancel = useRef(false); - const abortCtrl = useRef(new AbortController()); - const postMyCase = useCallback( - async (data: CasePostRequest) => { - try { - dispatch({ type: 'FETCH_INIT' }); - abortCtrl.current.abort(); - cancel.current = false; - abortCtrl.current = new AbortController(); - const response = await postCase(data, abortCtrl.current.signal); - if (!cancel.current) { - dispatch({ type: 'FETCH_SUCCESS' }); - } - return response; - } catch (error) { - if (!cancel.current) { + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); + + const postMyCase = useCallback(async (data: CasePostRequest) => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + + dispatch({ type: 'FETCH_INIT' }); + const response = await postCase(data, abortCtrlRef.current.signal); + + if (!isCancelledRef.current) { + dispatch({ type: 'FETCH_SUCCESS' }); + } + return response; + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: 'FETCH_FAILURE' }); } + dispatch({ type: 'FETCH_FAILURE' }); } - }, - [dispatchToaster] - ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); useEffect(() => { return () => { - abortCtrl.current.abort(); - cancel.current = true; + isCancelledRef.current = true; + abortCtrlRef.current.abort(); }; }, []); return { ...state, postCase: postMyCase }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx index 8fc8053c14f70..5eb875287ba88 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import { useReducer, useCallback } from 'react'; - +import { useReducer, useCallback, useRef, useEffect } from 'react'; import { CommentRequest } from '../../../../case/common/api'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; @@ -58,38 +57,47 @@ export const usePostComment = (): UsePostComment => { isError: false, }); const [, dispatchToaster] = useStateToaster(); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); const postMyComment = useCallback( async ({ caseId, data, updateCase, subCaseId }: PostComment) => { - let cancel = false; - const abortCtrl = new AbortController(); - try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); dispatch({ type: 'FETCH_INIT' }); - const response = await postComment(data, caseId, abortCtrl.signal, subCaseId); - if (!cancel) { + + const response = await postComment(data, caseId, abortCtrlRef.current.signal, subCaseId); + + if (!isCancelledRef.current) { dispatch({ type: 'FETCH_SUCCESS' }); if (updateCase) { updateCase(response); } } } catch (error) { - if (!cancel) { - errorToToaster({ - title: i18n.ERROR_TITLE, - error: error.body && error.body.message ? new Error(error.body.message) : error, - dispatchToaster, - }); + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + } dispatch({ type: 'FETCH_FAILURE' }); } } - return () => { - abortCtrl.abort(); - cancel = true; - }; }, [dispatchToaster] ); + useEffect(() => { + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; + }, []); + return { ...state, postComment: postMyComment }; }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx index 03d881d7934e9..27a02d9300cc0 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx @@ -67,17 +67,17 @@ export const usePostPushToService = (): UsePostPushToService => { }); const [, dispatchToaster] = useStateToaster(); const cancel = useRef(false); - const abortCtrl = useRef(new AbortController()); + const abortCtrlRef = useRef(new AbortController()); const pushCaseToExternalService = useCallback( async ({ caseId, connector }: PushToServiceRequest) => { try { - dispatch({ type: 'FETCH_INIT' }); - abortCtrl.current.abort(); + abortCtrlRef.current.abort(); cancel.current = false; - abortCtrl.current = new AbortController(); + abortCtrlRef.current = new AbortController(); + dispatch({ type: 'FETCH_INIT' }); - const response = await pushCase(caseId, connector.id, abortCtrl.current.signal); + const response = await pushCase(caseId, connector.id, abortCtrlRef.current.signal); if (!cancel.current) { dispatch({ type: 'FETCH_SUCCESS' }); @@ -90,11 +90,13 @@ export const usePostPushToService = (): UsePostPushToService => { return response; } catch (error) { if (!cancel.current) { - errorToToaster({ - title: i18n.ERROR_TITLE, - error: error.body && error.body.message ? new Error(error.body.message) : error, - dispatchToaster, - }); + if (error.name !== 'AbortError') { + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + } dispatch({ type: 'FETCH_FAILURE' }); } } @@ -105,7 +107,7 @@ export const usePostPushToService = (): UsePostPushToService => { useEffect(() => { return () => { - abortCtrl.current.abort(); + abortCtrlRef.current.abort(); cancel.current = true; }; }, []); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx index 23a23caeb71bd..e8de2257009e6 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import { useReducer, useCallback, useEffect, useRef } from 'react'; +import { useReducer, useCallback, useRef, useEffect } from 'react'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; - import { patchCase, patchSubCase } from './api'; import { UpdateKey, UpdateByKey, CaseStatuses } from './types'; import * as i18n from './translations'; @@ -70,8 +69,8 @@ export const useUpdateCase = ({ updateKey: null, }); const [, dispatchToaster] = useStateToaster(); - const abortCtrl = useRef(new AbortController()); - const didCancel = useRef(false); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); const dispatchUpdateCaseProperty = useCallback( async ({ @@ -84,24 +83,27 @@ export const useUpdateCase = ({ onError, }: UpdateByKey) => { try { - didCancel.current = false; - abortCtrl.current = new AbortController(); + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); dispatch({ type: 'FETCH_INIT', payload: updateKey }); + const response = await (updateKey === 'status' && subCaseId ? patchSubCase( caseId, subCaseId, { status: updateValue as CaseStatuses }, caseData.version, - abortCtrl.current.signal + abortCtrlRef.current.signal ) : patchCase( caseId, { [updateKey]: updateValue }, caseData.version, - abortCtrl.current.signal + abortCtrlRef.current.signal )); - if (!didCancel.current) { + + if (!isCancelledRef.current) { if (fetchCaseUserActions != null) { fetchCaseUserActions(caseId, subCaseId); } @@ -119,7 +121,7 @@ export const useUpdateCase = ({ } } } catch (error) { - if (!didCancel.current) { + if (!isCancelledRef.current) { if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, @@ -140,8 +142,8 @@ export const useUpdateCase = ({ useEffect(() => { return () => { - didCancel.current = true; - abortCtrl.current.abort(); + isCancelledRef.current = true; + abortCtrlRef.current.abort(); }; }, []); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx index e36b21823310e..81bce248852fe 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx @@ -5,10 +5,8 @@ * 2.0. */ -import { useReducer, useCallback } from 'react'; - +import { useReducer, useCallback, useRef, useEffect } from 'react'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; - import { patchComment } from './api'; import * as i18n from './translations'; import { Case } from './types'; @@ -72,6 +70,8 @@ export const useUpdateComment = (): UseUpdateComment => { isError: false, }); const [, dispatchToaster] = useStateToaster(); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); const dispatchUpdateComment = useCallback( async ({ @@ -83,41 +83,49 @@ export const useUpdateComment = (): UseUpdateComment => { updateCase, version, }: UpdateComment) => { - let cancel = false; - const abortCtrl = new AbortController(); try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); dispatch({ type: 'FETCH_INIT', payload: commentId }); + const response = await patchComment( caseId, commentId, commentUpdate, version, - abortCtrl.signal, + abortCtrlRef.current.signal, subCaseId ); - if (!cancel) { + + if (!isCancelledRef.current) { updateCase(response); fetchUserActions(); dispatch({ type: 'FETCH_SUCCESS', payload: { commentId } }); } } catch (error) { - if (!cancel) { - errorToToaster({ - title: i18n.ERROR_TITLE, - error: error.body && error.body.message ? new Error(error.body.message) : error, - dispatchToaster, - }); + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + } dispatch({ type: 'FETCH_FAILURE', payload: commentId }); } } - return () => { - cancel = true; - abortCtrl.abort(); - }; }, // eslint-disable-next-line react-hooks/exhaustive-deps [] ); + useEffect(() => { + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; + }, []); + return { ...state, patchComment: dispatchUpdateComment }; }; diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx index 11f424d83c530..a6dd64737f5ee 100644 --- a/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx @@ -8,14 +8,15 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { AndOrBadge } from './'; +const mockTheme = { eui: { euiColorLightShade: '#ece' } }; + describe('AndOrBadge', () => { test('it renders top and bottom antenna bars when "includeAntennas" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); @@ -27,7 +28,7 @@ describe('AndOrBadge', () => { test('it does not render top and bottom antenna bars when "includeAntennas" is false', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); @@ -39,7 +40,7 @@ describe('AndOrBadge', () => { test('it renders "and" when "type" is "and"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); @@ -49,7 +50,7 @@ describe('AndOrBadge', () => { test('it renders "or" when "type" is "or"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.test.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.test.tsx index 5bc89baa3d415..489d02990b1f4 100644 --- a/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.test.tsx @@ -6,29 +6,19 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { RoundedBadge } from './rounded_badge'; describe('RoundedBadge', () => { test('it renders "and" when "type" is "and"', () => { - const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - ); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('AND'); }); test('it renders "or" when "type" is "or"', () => { - const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - ); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('OR'); }); diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.test.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.test.tsx index 61cf44293005f..c6536a05be45d 100644 --- a/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.test.tsx @@ -8,14 +8,15 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { RoundedBadgeAntenna } from './rounded_badge_antenna'; +const mockTheme = { eui: { euiColorLightShade: '#ece' } }; + describe('RoundedBadgeAntenna', () => { test('it renders top and bottom antenna bars', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); @@ -27,7 +28,7 @@ describe('RoundedBadgeAntenna', () => { test('it renders "and" when "type" is "and"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); @@ -37,7 +38,7 @@ describe('RoundedBadgeAntenna', () => { test('it renders "or" when "type" is "or"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx index 6a3d82d47045a..79e6fe5506b84 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx @@ -6,9 +6,7 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { @@ -20,21 +18,19 @@ import { FieldComponent } from './field'; describe('FieldComponent', () => { test('it renders disabled if "isDisabled" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -44,21 +40,19 @@ describe('FieldComponent', () => { test('it renders loading if "isLoading" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find(`[data-test-subj="fieldAutocompleteComboBox"] button`).at(0).simulate('click'); expect( @@ -70,21 +64,19 @@ describe('FieldComponent', () => { test('it allows user to clear values if "isClearable" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -96,21 +88,19 @@ describe('FieldComponent', () => { test('it correctly displays selected field', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -121,21 +111,19 @@ describe('FieldComponent', () => { test('it invokes "onChange" when option selected', () => { const mockOnChange = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); ((wrapper.find(EuiComboBox).props() as unknown) as { diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_exists.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_exists.test.tsx index f577799827b89..b6300581f12dd 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_exists.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_exists.test.tsx @@ -6,19 +6,13 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { AutocompleteFieldExistsComponent } from './field_value_exists'; describe('AutocompleteFieldExistsComponent', () => { test('it renders field disabled', () => { - const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - ); + const wrapper = mount(); expect( wrapper diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx index e01cc5ff1e042..c605a71c50e33 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx @@ -6,9 +6,7 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { waitFor } from '@testing-library/react'; @@ -47,17 +45,15 @@ jest.mock('../../../lists_plugin_deps', () => { describe('AutocompleteFieldListsComponent', () => { test('it renders disabled if "isDisabled" is true', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -69,17 +65,15 @@ describe('AutocompleteFieldListsComponent', () => { test('it renders loading if "isLoading" is true', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper @@ -97,17 +91,15 @@ describe('AutocompleteFieldListsComponent', () => { test('it allows user to clear values if "isClearable" is true', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( wrapper @@ -118,17 +110,15 @@ describe('AutocompleteFieldListsComponent', () => { test('it correctly displays lists that match the selected "keyword" field esType', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="comboBoxToggleListButton"] button').simulate('click'); @@ -142,17 +132,15 @@ describe('AutocompleteFieldListsComponent', () => { test('it correctly displays lists that match the selected "ip" field esType', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="comboBoxToggleListButton"] button').simulate('click'); @@ -166,17 +154,15 @@ describe('AutocompleteFieldListsComponent', () => { test('it correctly displays selected list', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -190,17 +176,15 @@ describe('AutocompleteFieldListsComponent', () => { test('it invokes "onChange" when option selected', async () => { const mockOnChange = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); ((wrapper.find(EuiComboBox).props() as unknown) as { diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx index d4712092867e9..38d103fe65130 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx @@ -6,9 +6,7 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mount, ReactWrapper } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { EuiSuperSelect, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { act } from '@testing-library/react'; @@ -44,23 +42,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it renders row label if one passed in', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -70,23 +66,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it renders disabled if "isDisabled" is true', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -96,23 +90,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it renders loading if "isLoading" is true', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="valuesAutocompleteMatch"] button').at(0).simulate('click'); expect( @@ -124,23 +116,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it allows user to clear values if "isClearable" is true', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -152,23 +142,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it correctly displays selected value', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -179,23 +167,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it invokes "onChange" when new value created', async () => { const mockOnChange = jest.fn(); wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); ((wrapper.find(EuiComboBox).props() as unknown) as { @@ -208,23 +194,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it invokes "onChange" when new value selected', async () => { const mockOnChange = jest.fn(); wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); ((wrapper.find(EuiComboBox).props() as unknown) as { @@ -236,23 +220,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it refreshes autocomplete with search query when new value searched', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); act(() => { ((wrapper.find(EuiComboBox).props() as unknown) as { @@ -287,23 +269,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it displays only two options - "true" or "false"', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -326,23 +306,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it invokes "onChange" with "true" when selected', () => { const mockOnChange = jest.fn(); wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); ((wrapper.find(EuiSuperSelect).props() as unknown) as { @@ -355,23 +333,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it invokes "onChange" with "false" when selected', () => { const mockOnChange = jest.fn(); wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); ((wrapper.find(EuiSuperSelect).props() as unknown) as { @@ -396,23 +372,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it number input when field type is number', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -423,23 +397,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it invokes "onChange" with numeric value when inputted', () => { const mockOnChange = jest.fn(); wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx index aa2038262f40c..6b479c5ab8c4c 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx @@ -6,9 +6,7 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mount, ReactWrapper } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { act } from '@testing-library/react'; @@ -43,24 +41,22 @@ describe('AutocompleteFieldMatchAnyComponent', () => { test('it renders disabled if "isDisabled" is true', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -70,24 +66,22 @@ describe('AutocompleteFieldMatchAnyComponent', () => { test('it renders loading if "isLoading" is true', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find(`[data-test-subj="valuesAutocompleteMatchAny"] button`).at(0).simulate('click'); expect( @@ -99,24 +93,22 @@ describe('AutocompleteFieldMatchAnyComponent', () => { test('it allows user to clear values if "isClearable" is true', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -128,24 +120,22 @@ describe('AutocompleteFieldMatchAnyComponent', () => { test('it correctly displays selected value', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -156,24 +146,22 @@ describe('AutocompleteFieldMatchAnyComponent', () => { test('it invokes "onChange" when new value created', async () => { const mockOnChange = jest.fn(); wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); ((wrapper.find(EuiComboBox).props() as unknown) as { @@ -186,24 +174,22 @@ describe('AutocompleteFieldMatchAnyComponent', () => { test('it invokes "onChange" when new value selected', async () => { const mockOnChange = jest.fn(); wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); ((wrapper.find(EuiComboBox).props() as unknown) as { @@ -215,23 +201,21 @@ describe('AutocompleteFieldMatchAnyComponent', () => { test('it refreshes autocomplete with search query when new value searched', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); act(() => { ((wrapper.find(EuiComboBox).props() as unknown) as { diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.test.tsx index 56ae6d762e7ee..db16cbde2acb4 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.test.tsx @@ -6,9 +6,7 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; @@ -18,17 +16,15 @@ import { isOperator, isNotOperator } from './operators'; describe('OperatorComponent', () => { test('it renders disabled if "isDisabled" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -38,17 +34,15 @@ describe('OperatorComponent', () => { test('it renders loading if "isLoading" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find(`[data-test-subj="operatorAutocompleteComboBox"] button`).at(0).simulate('click'); expect( @@ -60,17 +54,15 @@ describe('OperatorComponent', () => { test('it allows user to clear values if "isClearable" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect(wrapper.find(`button[data-test-subj="comboBoxClearButton"]`).exists()).toBeTruthy(); @@ -78,18 +70,16 @@ describe('OperatorComponent', () => { test('it displays "operatorOptions" if param is passed in with items', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -99,18 +89,16 @@ describe('OperatorComponent', () => { test('it does not display "operatorOptions" if param is passed in with no items', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -145,17 +133,15 @@ describe('OperatorComponent', () => { test('it correctly displays selected operator', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -165,27 +151,25 @@ describe('OperatorComponent', () => { test('it only displays subset of operators if field type is nested', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -195,17 +179,15 @@ describe('OperatorComponent', () => { test('it only displays subset of operators if field type is boolean', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -221,17 +203,15 @@ describe('OperatorComponent', () => { test('it invokes "onChange" when option selected', () => { const mockOnChange = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); ((wrapper.find(EuiComboBox).props() as unknown) as { diff --git a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx index 096bea37566b3..6d87b5d3a68b9 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx @@ -6,10 +6,8 @@ */ import { Chart, BarSeries, Axis, ScaleType } from '@elastic/charts'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, ReactWrapper, shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { escapeDataProviderId } from '../drag_and_drop/helpers'; import { TestProviders } from '../../mock'; @@ -37,8 +35,6 @@ jest.mock('uuid', () => { }; }); -const theme = () => ({ eui: euiDarkVars, darkMode: true }); - const customHeight = '100px'; const customWidth = '120px'; const chartDataSets = [ @@ -323,11 +319,9 @@ describe.each(chartDataSets)('BarChart with stackByField', () => { beforeAll(() => { wrapper = mount( - - - - - + + + ); }); @@ -407,11 +401,9 @@ describe.each(chartDataSets)('BarChart with custom color', () => { beforeAll(() => { wrapper = mount( - - - - - + + + ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx index fb91a4a3ce92b..544f9b1abf8f2 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx @@ -5,10 +5,8 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; -import { ThemeProvider } from 'styled-components'; import '../../mock/match_media'; import '../../mock/react_beautiful_dnd'; @@ -26,8 +24,6 @@ jest.mock('@elastic/eui', () => { }; }); -const theme = () => ({ eui: euiDarkVars, darkMode: true }); - const allOthersDataProviderId = 'draggable-legend-item-527adabe-8e1c-4a1f-965c-2f3d65dda9e1-event_dataset-All others'; @@ -74,11 +70,9 @@ describe('DraggableLegend', () => { beforeEach(() => { wrapper = mount( - - - - - + + + ); }); @@ -120,11 +114,9 @@ describe('DraggableLegend', () => { it('does NOT render the legend when an empty collection of legendItems is provided', () => { const wrapper = mount( - - - - - + + + ); expect(wrapper.find('[data-test-subj="draggable-legend"]').exists()).toBe(false); @@ -132,11 +124,9 @@ describe('DraggableLegend', () => { it(`renders a legend with the minimum height when 'height' is zero`, () => { const wrapper = mount( - - - - - + + + ); expect(wrapper.find('[data-test-subj="draggable-legend"]').first()).toHaveStyleRule( diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx index 15c164e59557d..4958f6bae4a30 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx @@ -5,10 +5,8 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; -import { ThemeProvider } from 'styled-components'; import '../../mock/match_media'; import '../../mock/react_beautiful_dnd'; @@ -25,8 +23,6 @@ jest.mock('@elastic/eui', () => { }; }); -const theme = () => ({ eui: euiDarkVars, darkMode: true }); - describe('DraggableLegendItem', () => { describe('rendering a regular (non "All others") legend item', () => { const legendItem: LegendItem = { @@ -41,11 +37,9 @@ describe('DraggableLegendItem', () => { beforeEach(() => { wrapper = mount( - - - - - + + + ); }); @@ -79,11 +73,9 @@ describe('DraggableLegendItem', () => { beforeEach(() => { wrapper = mount( - - - - - + + + ); }); @@ -118,11 +110,9 @@ describe('DraggableLegendItem', () => { }; const wrapper = mount( - - - - - + + + ); expect(wrapper.find('[data-test-subj="legend-color"]').exists()).toBe(false); diff --git a/x-pack/plugins/security_solution/public/common/components/empty_value/empty_value.test.tsx b/x-pack/plugins/security_solution/public/common/components/empty_value/empty_value.test.tsx index 764d9109816b5..e3c74bf425628 100644 --- a/x-pack/plugins/security_solution/public/common/components/empty_value/empty_value.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/empty_value/empty_value.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, shallow } from 'enzyme'; import React from 'react'; import { ThemeProvider } from 'styled-components'; @@ -21,7 +20,7 @@ import { } from '.'; describe('EmptyValue', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); + const mockTheme = { eui: { euiColorMediumShade: '#ece' } }; test('it renders against snapshot', () => { const wrapper = shallow(

{getEmptyString()}

); @@ -35,7 +34,7 @@ describe('EmptyValue', () => { describe('#getEmptyString', () => { test('should turn into an empty string place holder', () => { const wrapper = mountWithIntl( - +

{getEmptyString()}

); @@ -45,7 +44,7 @@ describe('EmptyValue', () => { describe('#getEmptyTagValue', () => { const wrapper = mount( - +

{getEmptyTagValue()}

); @@ -55,7 +54,7 @@ describe('EmptyValue', () => { describe('#getEmptyStringTag', () => { test('should turn into an span that has length of 1', () => { const wrapper = mountWithIntl( - +

{getEmptyStringTag()}

); @@ -64,7 +63,7 @@ describe('EmptyValue', () => { test('should turn into an empty string tag place holder', () => { const wrapper = mountWithIntl( - +

{getEmptyStringTag()}

); @@ -75,7 +74,7 @@ describe('EmptyValue', () => { describe('#defaultToEmptyTag', () => { test('should default to an empty value when a value is null', () => { const wrapper = mount( - +

{defaultToEmptyTag(null)}

); @@ -84,7 +83,7 @@ describe('EmptyValue', () => { test('should default to an empty value when a value is undefined', () => { const wrapper = mount( - +

{defaultToEmptyTag(undefined)}

); @@ -114,7 +113,7 @@ describe('EmptyValue', () => { }, }; const wrapper = mount( - +

{getOrEmptyTag('a.b.c', test)}

); @@ -130,7 +129,7 @@ describe('EmptyValue', () => { }, }; const wrapper = mount( - +

{getOrEmptyTag('a.b.c', test)}

); @@ -144,7 +143,7 @@ describe('EmptyValue', () => { }, }; const wrapper = mount( - +

{getOrEmptyTag('a.b.c', test)}

); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index af76a79f0e330..9ba6fe104be45 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mount, ReactWrapper } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { waitFor } from '@testing-library/react'; import { AddExceptionModal } from './'; @@ -32,6 +31,17 @@ import { import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; import { AlertData } from '../types'; +const mockTheme = { + eui: { + euiBreakpoints: { + l: '1200px', + }, + paddingSizes: { + m: '10px', + }, + }, +}; + jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index'); jest.mock('../../../../common/lib/kibana'); jest.mock('../../../containers/source'); @@ -101,7 +111,7 @@ describe('When the add exception modal is opened', () => { }, ]); wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { let wrapper: ReactWrapper; beforeEach(async () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { file: { path: 'test/path' }, }; wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { file: { path: 'test/path' }, }; wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { file: { path: 'test/path' }, }; wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { file: { path: 'test/path' }, }; wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('when there are exception builder errors submit button is disabled', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders exceptionItemEntryFirstRowAndBadge for very first exception item in builder', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); @@ -27,7 +28,7 @@ describe('BuilderAndBadgeComponent', () => { test('it renders exceptionItemEntryInvisibleAndBadge if "entriesLength" is 1 or less', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); @@ -39,7 +40,7 @@ describe('BuilderAndBadgeComponent', () => { test('it renders regular "and" badge if exception item is not the first one and includes more than one entry', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx index 6505c5eb2b310..1ea54473032cc 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { useKibana } from '../../../../common/lib/kibana'; import { fields } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; @@ -18,6 +17,12 @@ import { getEntryMatchAnyMock } from '../../../../../../lists/common/schemas/typ import { BuilderExceptionListItemComponent } from './exception_item'; +const mockTheme = { + eui: { + euiColorLightShade: '#ece', + }, +}; + jest.mock('../../../../common/lib/kibana'); describe('BuilderExceptionListItemComponent', () => { @@ -46,7 +51,7 @@ describe('BuilderExceptionListItemComponent', () => { entries: [getEntryMatchMock(), getEntryMatchMock()], }; const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock(), getEntryMatchMock()]; const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock()]; const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock()]; const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { @@ -50,7 +55,7 @@ describe('ExceptionBuilderComponent', () => { test('it displays empty entry if no "exceptionListItems" are passed in', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it displays "exceptionListItems" that are passed in', async () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it displays "or", "and" and "add nested button" enabled', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it adds an entry when "and" clicked', async () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it adds an exception item when "or" clicked', async () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it displays empty entry if user deletes last remaining entry', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it displays "and" badge if at least one exception item includes more than one entry', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it does not display "and" badge if none of the exception items include more than one entry', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { describe('nested entry', () => { test('it adds a nested entry when "add nested entry" clicked', async () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { }, ]); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { ] as EntriesArray, }; wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { let wrapper: ReactWrapper; beforeEach(async () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { }, })); wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { let wrapper: ReactWrapper; beforeEach(async () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { beforeEach(async () => { const exceptionItemMock = { ...getExceptionListItemSchemaMock(), entries: [] }; wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('when there are exception builder errors has the add exception button disabled', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { it('it renders error details', () => { const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -57,21 +53,19 @@ describe('ErrorCallout', () => { it('it invokes "onCancel" when cancel button clicked', () => { const mockOnCancel = jest.fn(); const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="errorCalloutCancelButton"]').at(0).simulate('click'); @@ -81,21 +75,19 @@ describe('ErrorCallout', () => { it('it does not render status code if not available', () => { const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -109,21 +101,19 @@ describe('ErrorCallout', () => { it('it renders specific missing exceptions list error', () => { const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -137,21 +127,19 @@ describe('ErrorCallout', () => { it('it dissasociates list from rule when remove exception list clicked ', () => { const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').at(0).simulate('click'); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx index c7d7d2d39393c..b96ae5c06dd22 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx @@ -8,13 +8,18 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import moment from 'moment-timezone'; import { ExceptionDetails } from './exception_details'; import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getCommentsArrayMock } from '../../../../../../../lists/common/schemas/types/comment.mock'; +const mockTheme = { + eui: { + euiColorLightestShade: '#ece', + }, +}; + describe('ExceptionDetails', () => { beforeEach(() => { moment.tz.setDefault('UTC'); @@ -29,7 +34,7 @@ describe('ExceptionDetails', () => { exceptionItem.comments = []; const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.comments = [getCommentsArrayMock()[0]]; const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders the operating system if one is specified in the exception item', () => { const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders the exception item creator', () => { const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders the exception item creation timestamp', () => { const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders the description if one is included on the exception item', () => { const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it does NOT render the and badge if only one exception item entry exists', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders the and badge if more than one exception item exists', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it invokes "onEdit" when edit button clicked', () => { const mockOnEdit = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it invokes "onDelete" when delete button clicked', () => { const mockOnDelete = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders edit button disabled if "disableDelete" is "true"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders delete button in loading state if "disableDelete" is "true"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { parentEntry.value = undefined; const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders non-nested entries', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { it('it renders ExceptionDetails and ExceptionEntries', () => { const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { it('it renders passed in "pageSize" as selected option', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect(wrapper.find('[data-test-subj="exceptionsPerPageBtn"]').at(0).text()).toEqual( @@ -35,17 +31,15 @@ describe('ExceptionsViewerPagination', () => { it('it renders all passed in page size options when per page button clicked', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="exceptionsPerPageBtn"] button').simulate('click'); @@ -64,17 +58,15 @@ describe('ExceptionsViewerPagination', () => { it('it invokes "onPaginationChange" when per page item is clicked', () => { const mockOnPaginationChange = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="exceptionsPerPageBtn"] button').simulate('click'); @@ -87,17 +79,15 @@ describe('ExceptionsViewerPagination', () => { it('it renders correct total page count', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect(wrapper.find('[data-test-subj="exceptionsPagination"]').at(0).prop('pageCount')).toEqual( @@ -111,17 +101,15 @@ describe('ExceptionsViewerPagination', () => { it('it invokes "onPaginationChange" when next clicked', () => { const mockOnPaginationChange = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="pagination-button-next"]').at(1).simulate('click'); @@ -134,17 +122,15 @@ describe('ExceptionsViewerPagination', () => { it('it invokes "onPaginationChange" when page clicked', () => { const mockOnPaginationChange = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('button[data-test-subj="pagination-button-3"]').simulate('click'); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx index 6167a29a4a17d..42ce0c792dfa3 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx @@ -8,14 +8,25 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mountWithIntl } from '@kbn/test/jest'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { ExceptionsViewerUtility } from './exceptions_utility'; +const mockTheme = { + eui: { + euiBreakpoints: { + l: '1200px', + }, + paddingSizes: { + m: '10px', + }, + euiBorderThin: '1px solid #ece', + }, +}; + describe('ExceptionsViewerUtility', () => { it('it renders correct pluralized text when more than one exception exists', () => { const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> + { it('it renders correct singular text when less than two exceptions exists', () => { const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> + { it('it invokes "onRefreshClick" when refresh button clicked', () => { const mockOnRefreshClick = jest.fn(); const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> + { it('it does not render any messages when "showEndpointList" and "showDetectionsList" are "false"', () => { const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> + { it('it does render detections messages when "showDetectionsList" is "true"', () => { const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> + { it('it does render endpoint messages when "showEndpointList" is "true"', () => { const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> + { it('it renders all disabled if "isInitLoading" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -47,16 +43,14 @@ describe('ExceptionsViewerHeader', () => { it('it displays toggles and add exception popover when more than one list type available', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect(wrapper.find('[data-test-subj="exceptionsFilterGroupBtns"]').exists()).toBeTruthy(); @@ -67,16 +61,14 @@ describe('ExceptionsViewerHeader', () => { it('it does not display toggles and add exception popover if only one list type is available', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect(wrapper.find('[data-test-subj="exceptionsFilterGroupBtns"]')).toHaveLength(0); @@ -87,16 +79,14 @@ describe('ExceptionsViewerHeader', () => { it('it displays add exception button without popover if only one list type is available', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -107,16 +97,14 @@ describe('ExceptionsViewerHeader', () => { it('it renders detections filter toggle selected when clicked', () => { const mockOnFilterChange = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="exceptionsDetectionFilterBtn"] button').simulate('click'); @@ -149,16 +137,14 @@ describe('ExceptionsViewerHeader', () => { it('it renders endpoint filter toggle selected and invokes "onFilterChange" when clicked', () => { const mockOnFilterChange = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="exceptionsEndpointFilterBtn"] button').simulate('click'); @@ -191,16 +177,14 @@ describe('ExceptionsViewerHeader', () => { it('it invokes "onAddExceptionClick" when user selects to add an exception item and only endpoint exception lists are available', () => { const mockOnAddExceptionClick = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"] button').simulate('click'); @@ -211,16 +195,14 @@ describe('ExceptionsViewerHeader', () => { it('it invokes "onAddDetectionsExceptionClick" when user selects to add an exception item and only endpoint detections lists are available', () => { const mockOnAddExceptionClick = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"] button').simulate('click'); @@ -231,16 +213,14 @@ describe('ExceptionsViewerHeader', () => { it('it invokes "onAddEndpointExceptionClick" when user selects to add an exception item to endpoint list from popover', () => { const mockOnAddExceptionClick = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper @@ -254,16 +234,14 @@ describe('ExceptionsViewerHeader', () => { it('it invokes "onAddDetectionsExceptionClick" when user selects to add an exception item to endpoint list from popover', () => { const mockOnAddExceptionClick = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper @@ -277,16 +255,14 @@ describe('ExceptionsViewerHeader', () => { it('it invokes "onFilterChange" when search used and "Enter" pressed', () => { const mockOnFilterChange = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('EuiFieldSearch').at(0).simulate('keyup', { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx index 3171735d905de..167b95995212b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx @@ -8,27 +8,32 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import * as i18n from '../translations'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { ExceptionsViewerItems } from './exceptions_viewer_items'; +const mockTheme = { + eui: { + euiSize: '10px', + euiColorPrimary: '#ece', + euiColorDanger: '#ece', + }, +}; + describe('ExceptionsViewerItems', () => { it('it renders empty prompt if "showEmpty" is "true"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); @@ -43,7 +48,7 @@ describe('ExceptionsViewerItems', () => { it('it renders no search results found prompt if "showNoResults" is "true"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { it('it renders exceptions if "showEmpty" and "isInitLoading" is "false", and exceptions exist', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { it('it does not render exceptions if "isInitLoading" is "true"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { exception2.id = 'newId'; const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { exception2.id = 'newId'; const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const mockOnDeleteException = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { jest.fn(), ]); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { it('it renders empty prompt if no "exceptionListMeta" passed in', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { ]); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); const closeModal = jest.fn(); describe('rendering', () => { test('when isShowing is positive and request and response are not null', () => { const wrapper = mount( - + { describe('functionality from tab statistics/request/response', () => { test('Click on statistic Tab', () => { const wrapper = mount( - + { test('Click on request Tab', () => { const wrapper = mount( - + { test('Click on response Tab', () => { const wrapper = mount( - + { describe('events', () => { test('Make sure that toggle function has been called when you click on the close button', () => { const wrapper = mount( - + - +

My test supplement.

- } - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={ - Array [ - Object { - "numberOfRow": 2, - "text": "2 rows", - }, - Object { - "numberOfRow": 5, - "text": "5 rows", - }, - Object { - "numberOfRow": 10, - "text": "10 rows", - }, - Object { - "numberOfRow": 20, - "text": "20 rows", - }, - Object { - "numberOfRow": 50, - "text": "50 rows", - }, - ] - } - limit={1} - loadPage={[MockFunction]} - loading={false} - pageOfItems={ - Array [ - Object { - "cursor": Object { - "value": "98966fa2013c396155c460d35c0902be", - }, - "host": Object { - "_id": "cPsuhGcB0WOhS6qyTKC0", - "firstSeen": "2018-12-06T15:40:53.319Z", - "name": "elrond.elstc.co", - "os": "Ubuntu", - "version": "18.04.1 LTS (Bionic Beaver)", - }, - }, - Object { - "cursor": Object { - "value": "aa7ca589f1b8220002f2fc61c64cfbf1", - }, - "host": Object { - "_id": "KwQDiWcB0WOhS6qyXmrW", - "firstSeen": "2018-12-07T14:12:38.560Z", - "name": "siem-kibana", - "os": "Debian GNU/Linux", - "version": "9 (stretch)", - }, - }, - ] - } - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={[MockFunction]} - updateLimitPagination={[Function]} - /> - +
+ + + + + Rows per page: 1 + + } + closePopover={[Function]} + data-test-subj="loadingMoreSizeRowPopover" + display="inlineBlock" + hasArrow={true} + id="customizablePagination" + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + repositionOnScroll={true} + > + + 2 rows + , + + 5 rows + , + + 10 rows + , + + 20 rows + , + + 50 rows + , + ] + } + /> + + + + + + + + `; diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx index 57d4c8451de24..c20f1ae66c797 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx @@ -14,7 +14,6 @@ import { Direction } from '../../../graphql/types'; import { BasicTableProps, PaginatedTable } from './index'; import { getHostsColumns, mockData, rowItems, sortedHosts } from './index.mock'; import { ThemeProvider } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; jest.mock('react', () => { const r = jest.requireActual('react'); @@ -22,8 +21,20 @@ jest.mock('react', () => { return { ...r, memo: (x: any) => x }; }); +const mockTheme = { + eui: { + euiColorEmptyShade: '#ece', + euiSizeL: '10px', + euiBreakpoints: { + s: '450px', + }, + paddingSizes: { + m: '10px', + }, + }, +}; + describe('Paginated Table Component', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); let loadPage: jest.Mock; let updateLimitPagination: jest.Mock; let updateActivePage: jest.Mock; @@ -36,26 +47,24 @@ describe('Paginated Table Component', () => { describe('rendering', () => { test('it renders the default load more table', () => { const wrapper = shallow( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> -
+ {'My test supplement.'}

} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={1} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={(limit) => updateLimitPagination({ limit })} + /> ); expect(wrapper).toMatchSnapshot(); @@ -63,7 +72,7 @@ describe('Paginated Table Component', () => { test('it renders the loading panel at the beginning ', () => { const wrapper = mount( - + { test('it renders the over loading panel after data has been in the table ', () => { const wrapper = mount( - + { test('it renders the correct amount of pages and starts at activePage: 0', () => { const wrapper = mount( - + { test('it render popover to select new limit in table', () => { const wrapper = mount( - + { test('it will NOT render popover to select new limit in table if props itemsPerRow is empty', () => { const wrapper = mount( - + { test('It should render a sort icon if sorting is defined', () => { const mockOnChange = jest.fn(); const wrapper = mount( - + { test('Should display toast when user reaches end of results max', () => { const wrapper = mount( - + { test('Should show items per row if totalCount is greater than items', () => { const wrapper = mount( - + { test('Should hide items per row if totalCount is less than items', () => { const wrapper = mount( - + { describe('Events', () => { test('should call updateActivePage with 1 when clicking to the first page', () => { const wrapper = mount( - + { test('Should call updateActivePage with 0 when you pick a new limit', () => { const wrapper = mount( - + { // eslint-disable-next-line @typescript-eslint/no-explicit-any const ComponentWithContext = (props: BasicTableProps) => { return ( - + ); @@ -424,7 +433,7 @@ describe('Paginated Table Component', () => { test('Should call updateLimitPagination when you pick a new limit', () => { const wrapper = mount( - + { test('Should call onChange when you choose a new sort in the table', () => { const mockOnChange = jest.fn(); const wrapper = mount( - + { }); describe('Stat Items Component', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); + const mockTheme = { eui: { euiColorMediumShade: '#ece' } }; const state: State = mockGlobalState; const { storage } = createSecuritySolutionStorageMock(); const store = createStore( @@ -71,7 +70,7 @@ describe('Stat Items Component', () => { describe.each([ [ mount( - + { ], [ mount( - + { test('it renders entryItemIndexItemEntryFirstRowAndBadge for very first item', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); @@ -25,7 +30,7 @@ describe('AndBadgeComponent', () => { test('it renders entryItemEntryInvisibleAndBadge if "entriesLength" is 1 or less', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); @@ -37,7 +42,7 @@ describe('AndBadgeComponent', () => { test('it renders regular "and" badge if item is not the first one and includes more than one entry', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/index.test.tsx index 73174bc5fc113..6aa33c3bcf4ca 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/index.test.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { waitFor } from '@testing-library/react'; import { fields } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; @@ -19,6 +18,12 @@ import { ThreatMatchComponent } from './'; import { ThreatMapEntries } from './types'; import { IndexPattern } from 'src/plugins/data/public'; +const mockTheme = { + eui: { + euiColorLightShade: '#ece', + }, +}; + jest.mock('../../../common/lib/kibana'); const getPayLoad = (): ThreatMapEntries[] => [ @@ -51,7 +56,7 @@ describe('ThreatMatchComponent', () => { test('it displays empty entry if no "listItems" are passed in', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it displays "Search" for "listItems" that are passed in', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it displays "or", "and" enabled', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it adds an entry when "and" clicked', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it adds an item when "or" clicked', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it removes one row if user deletes a row', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it displays "and" badge if at least one item includes more than one entry', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it does not display "and" badge if none of the items include more than one entry', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ({ @@ -66,7 +71,7 @@ describe('ListItemComponent', () => { describe('and badge logic', () => { test('it renders "and" badge with extra top padding for the first item when "andLogicIncluded" is "true"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders "and" badge when more than one item entry exists and it is not the first item', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders indented "and" badge when "andLogicIncluded" is "true" and only one entry exists', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders no "and" badge when "andLogicIncluded" is "false"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders loader when isLoading is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); @@ -58,23 +54,21 @@ describe('PreviewCustomQueryHistogram', () => { test('it configures data and subtitle', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); @@ -111,19 +105,17 @@ describe('PreviewCustomQueryHistogram', () => { const mockRefetch = jest.fn(); mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(mockSetQuery).toHaveBeenCalledWith({ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx index 65bb029e2e32f..df6a8975a5b97 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx @@ -6,9 +6,7 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import * as i18n from './translations'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; @@ -35,19 +33,17 @@ describe('PreviewEqlQueryHistogram', () => { test('it renders loader when isLoading is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); @@ -58,23 +54,21 @@ describe('PreviewEqlQueryHistogram', () => { test('it configures data and subtitle', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); @@ -109,19 +103,17 @@ describe('PreviewEqlQueryHistogram', () => { const mockRefetch = jest.fn(); mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(mockSetQuery).toHaveBeenCalledWith({ @@ -134,23 +126,21 @@ describe('PreviewEqlQueryHistogram', () => { test('it displays histogram', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx index d9bd32ce082ca..85e31e7ed36e5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx @@ -6,9 +6,7 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { TestProviders } from '../../../../common/mock'; import { PreviewHistogram } from './histogram'; @@ -17,19 +15,17 @@ import { getHistogramConfig } from './helpers'; describe('PreviewHistogram', () => { test('it renders loading icon if "isLoading" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); @@ -38,40 +34,38 @@ describe('PreviewHistogram', () => { test('it renders chart if "isLoading" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx index bb87242d9bf10..700c2d516b995 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx @@ -9,7 +9,6 @@ import React from 'react'; import { of } from 'rxjs'; import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { TestProviders } from '../../../../common/mock'; import { useKibana } from '../../../../common/lib/kibana'; @@ -18,6 +17,12 @@ import { getMockEqlResponse } from '../../../../common/hooks/eql/eql_search_resp import { useMatrixHistogram } from '../../../../common/containers/matrix_histogram'; import { useEqlPreview } from '../../../../common/hooks/eql/'; +const mockTheme = { + eui: { + euiSuperDatePickerWidth: '180px', + }, +}; + jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../common/containers/matrix_histogram'); jest.mock('../../../../common/hooks/eql/'); @@ -63,7 +68,7 @@ describe('PreviewQuery', () => { test('it renders timeframe select and preview button on render', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders preview button disabled if "isDisabled" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders preview button disabled if "query" is undefined', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders query histogram when rule type is query and preview button clicked', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders noise warning when rule type is query, timeframe is last hour and hit average is greater than 1/hour', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders query histogram when rule type is saved_query and preview button clicked', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders eql histogram when preview button clicked and rule type is eql', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders noise warning when rule type is eql, timeframe is last hour and hit average is greater than 1/hour', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders threshold histogram when preview button clicked, rule type is threshold, and threshold field is defined', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders noise warning when rule type is threshold, and threshold field is defined, timeframe is last hour and hit average is greater than 1/hour', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is not defined', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is empty string', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it hides histogram when timeframe changes', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders loader when isLoading is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); @@ -51,20 +47,18 @@ describe('PreviewThresholdQueryHistogram', () => { test('it configures buckets data', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); @@ -86,20 +80,18 @@ describe('PreviewThresholdQueryHistogram', () => { const mockRefetch = jest.fn(); mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(mockSetQuery).toHaveBeenCalledWith({ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx index 71670658c88a9..fc91c26148c17 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx @@ -7,8 +7,6 @@ import { mount } from 'enzyme'; import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { waitFor } from '@testing-library/react'; import { enableRules } from '../../../containers/detection_engine/rules'; @@ -34,9 +32,7 @@ describe('RuleSwitch', () => { test('it renders loader if "isLoading" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect(wrapper.find('[data-test-subj="ruleSwitchLoader"]').exists()).toBeTruthy(); @@ -45,42 +41,27 @@ describe('RuleSwitch', () => { test('it renders switch disabled if "isDisabled" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(0).props().disabled).toBeTruthy(); }); test('it renders switch enabled if "enabled" is true', () => { - const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - ); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(0).props().checked).toBeTruthy(); }); test('it renders switch disabled if "enabled" is false', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(0).props().checked).toBeFalsy(); }); test('it renders an off switch enabled on click', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); @@ -96,9 +77,7 @@ describe('RuleSwitch', () => { (enableRules as jest.Mock).mockResolvedValue([rule]); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); @@ -113,14 +92,7 @@ describe('RuleSwitch', () => { (enableRules as jest.Mock).mockRejectedValue(mockError); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); @@ -138,14 +110,7 @@ describe('RuleSwitch', () => { ]); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); @@ -157,15 +122,13 @@ describe('RuleSwitch', () => { test('it invokes "enableRulesAction" if dispatch is passed through', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx index edd5c0d4e6e4c..c1773b2fffbab 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { ThemeProvider } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_light.json'; import { act } from '@testing-library/react'; import { stubIndexPattern } from 'src/plugins/data/common/index_patterns/index_pattern.stub'; @@ -24,8 +23,13 @@ import { } from '../../../pages/detection_engine/rules/types'; import { fillEmptySeverityMappings } from '../../../pages/detection_engine/rules/helpers'; +const mockTheme = { + eui: { + euiColorLightestShade: '#ece', + }, +}; + jest.mock('../../../../common/containers/source'); -const theme = () => ({ eui: euiDarkVars, darkMode: true }); jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { @@ -37,6 +41,7 @@ jest.mock('@elastic/eui', () => { }, }; }); + describe('StepAboutRuleComponent', () => { let formHook: RuleStepsFormHooks[RuleStep.aboutRule] | null = null; const setFormHook = ( @@ -72,7 +77,7 @@ describe('StepAboutRuleComponent', () => { it('is invalid if description is not present', async () => { const wrapper = mount( - + { it('is invalid if no "name" is present', async () => { const wrapper = mount( - + { it('is valid if both "name" and "description" are present', async () => { const wrapper = mount( - + { it('it allows user to set the risk score as a number (and not a string)', async () => { const wrapper = mount( - + { it('does not modify the provided risk score until the user changes the severity', async () => { const wrapper = mount( - + ({ eui: euiDarkVars, darkMode: true }); +const mockTheme = { + eui: { euiSizeL: '10px', euiBreakpoints: { s: '450px' }, paddingSizes: { m: '10px' } }, +}; describe('StepAboutRuleToggleDetails', () => { let mockRule: AboutStepRule; @@ -93,7 +94,7 @@ describe('StepAboutRuleToggleDetails', () => { describe('note value does exist', () => { test('it renders toggle buttons, defaulted to "details"', () => { const wrapper = mount( - + { test('it allows users to toggle between "details" and "note"', () => { const wrapper = mount( - + { test('it displays notes markdown when user toggles to "notes"', () => { const wrapper = mount( - + ({ eui: euiDarkVars, darkMode: true }); +const mockTheme = { eui: { euiBreakpoints: { l: '1200px' }, paddingSizes: { m: '10px' } } }; describe('AllRules', () => { it('renders AllRulesUtilityBar total rules and selected rules', () => { const wrapper = mount( - + { it('does not render total selected and bulk actions when "showBulkActions" is false', () => { const wrapper = mount( - + { it('renders utility actions if user has permissions', () => { const wrapper = mount( - + { it('renders no utility actions if user has no permissions', () => { const wrapper = mount( - + { it('invokes refresh on refresh action click', () => { const mockRefresh = jest.fn(); const wrapper = mount( - + { it('invokes onRefreshSwitch when auto refresh switch is clicked', async () => { const mockSwitch = jest.fn(); const wrapper = mount( - + ({ htmlIdGenerator: () => () => 'mockId', })); @@ -32,9 +37,7 @@ const now = 111111; const renderList = (store: ReturnType) => { const Wrapper: React.FC = ({ children }) => ( - ({ eui: euiLightVars, darkMode: false })}> - {children} - + {children} ); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap index 8f70c61ba4afc..5a176018f0e3f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap @@ -486,12 +486,7 @@ exports[`TrustedAppsList renders correctly when failed loading data for the seco exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` .c0 { - background-color: #f5f7fa; - padding: 16px; -} - -.c3 { - padding: 16px; + background-color: #ece; } .c1.c1.c1 { @@ -864,7 +859,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
({ @@ -32,9 +37,7 @@ const now = 111111; const renderList = (store: ReturnType) => { const Wrapper: React.FC = ({ children }) => ( - ({ eui: euiLightVars, darkMode: false })}> - {children} - + {children} ); diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx index 4b62139a8679f..0ec12a00d578b 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx @@ -5,10 +5,8 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; -import { ThemeProvider } from 'styled-components'; import '../../../common/mock/match_media'; import '../../../common/mock/react_beautiful_dnd'; @@ -24,7 +22,6 @@ jest.mock('../../../common/containers/matrix_histogram', () => ({ useMatrixHistogram: jest.fn(), })); -const theme = () => ({ eui: { ...euiDarkVars, euiSizeL: '24px' }, darkMode: true }); const from = '2020-03-31T06:00:00.000Z'; const to = '2019-03-31T06:00:00.000Z'; @@ -55,11 +52,9 @@ describe('Alerts by category', () => { ]); wrapper = mount( - - - - - + + + ); await waitFor(() => { @@ -123,11 +118,9 @@ describe('Alerts by category', () => { ]); wrapper = mount( - - - - - + + + ); wrapper.update(); diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx index ae8a00d2b4aa0..004e675cb3516 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx @@ -113,7 +113,7 @@ const StatefulRecentTimelinesComponent: React.FC = ({ apolloClient, filte )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.test.tsx index 278e01bcd8923..0f7a2070b8ef4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.test.tsx @@ -7,8 +7,6 @@ import { mount } from 'enzyme'; import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mockBrowserFields } from '../../../common/containers/source/mock'; @@ -19,18 +17,15 @@ import * as i18n from './translations'; const timelineId = 'test'; describe('CategoriesPane', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); test('it renders the expected title', () => { const wrapper = mount( - - - + ); expect(wrapper.find('[data-test-subj="categories-pane-title"]').first().text()).toEqual( @@ -40,15 +35,13 @@ describe('CategoriesPane', () => { test('it renders a "No fields match" message when filteredBrowserFields is empty', () => { const wrapper = mount( - - - + ); expect(wrapper.find('[data-test-subj="categories-container"] tbody').first().text()).toEqual( diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.test.tsx index 44b65185627ff..7b00b768b56a0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.test.tsx @@ -12,25 +12,20 @@ import { mockBrowserFields } from '../../../common/containers/source/mock'; import { CATEGORY_PANE_WIDTH, getFieldCount } from './helpers'; import { CategoriesPane } from './categories_pane'; -import { ThemeProvider } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; const timelineId = 'test'; -const theme = () => ({ eui: euiDarkVars, darkMode: true }); describe('getCategoryColumns', () => { Object.keys(mockBrowserFields).forEach((categoryId) => { test(`it renders the ${categoryId} category name (from filteredBrowserFields)`, () => { const wrapper = mount( - - - + ); const fieldCount = Object.keys(mockBrowserFields[categoryId].fields ?? {}).length; @@ -44,15 +39,13 @@ describe('getCategoryColumns', () => { Object.keys(mockBrowserFields).forEach((categoryId) => { test(`it renders the correct field count for the ${categoryId} category (from filteredBrowserFields)`, () => { const wrapper = mount( - - - + ); expect( @@ -65,15 +58,13 @@ describe('getCategoryColumns', () => { const selectedCategoryId = 'auditd'; const wrapper = mount( - - - + ); expect( @@ -89,15 +80,13 @@ describe('getCategoryColumns', () => { const notTheSelectedCategoryId = 'base'; const wrapper = mount( - - - + ); expect( @@ -115,15 +104,13 @@ describe('getCategoryColumns', () => { const onCategorySelected = jest.fn(); const wrapper = mount( - - - + ); wrapper diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx index 49f68281ae103..c130ea4c96814 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx @@ -7,12 +7,26 @@ import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; +import { useParams } from 'react-router-dom'; import { DeleteTimelineModal } from './delete_timeline_modal'; import * as i18n from '../translations'; +import { TimelineType } from '../../../../../common/types/timeline'; + +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { + ...actual, + useParams: jest.fn(), + }; +}); describe('DeleteTimelineModal', () => { + beforeAll(() => { + (useParams as jest.Mock).mockReturnValue({ tabName: TimelineType.default }); + }); + test('it renders the expected title when a timeline is selected', () => { const wrapper = mountWithIntl( { /> ); - expect(wrapper.find('[data-test-subj="warning"]').first().text()).toEqual(i18n.DELETE_WARNING); + expect(wrapper.find('[data-test-subj="warning"]').first().text()).toEqual( + i18n.DELETE_TIMELINE_WARNING + ); }); test('it invokes closeModal when the Cancel button is clicked', () => { @@ -115,3 +131,23 @@ describe('DeleteTimelineModal', () => { expect(onDelete).toBeCalled(); }); }); + +describe('DeleteTimelineTemplateModal', () => { + beforeAll(() => { + (useParams as jest.Mock).mockReturnValue({ tabName: TimelineType.template }); + }); + + test('it renders a deletion warning', () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find('[data-test-subj="warning"]').first().text()).toEqual( + i18n.DELETE_TIMELINE_TEMPLATE_WARNING + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx index 5538610487899..f0efda6528507 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx @@ -10,7 +10,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback } from 'react'; import { isEmpty } from 'lodash/fp'; +import { useParams } from 'react-router-dom'; import * as i18n from '../translations'; +import { TimelineType } from '../../../../../common/types/timeline'; interface Props { title?: string | null; @@ -24,6 +26,12 @@ export const DELETE_TIMELINE_MODAL_WIDTH = 600; // px * Renders a modal that confirms deletion of a timeline */ export const DeleteTimelineModal = React.memo(({ title, closeModal, onDelete }) => { + const { tabName } = useParams<{ tabName: TimelineType }>(); + const warning = + tabName === TimelineType.template + ? i18n.DELETE_TIMELINE_TEMPLATE_WARNING + : i18n.DELETE_TIMELINE_WARNING; + const getTitle = useCallback(() => { const trimmedTitle = title != null ? title.trim() : ''; const titleResult = !isEmpty(trimmedTitle) ? trimmedTitle : i18n.UNTITLED_TIMELINE; @@ -48,7 +56,7 @@ export const DeleteTimelineModal = React.memo(({ title, closeModal, onDel onConfirm={onDelete} title={getTitle()} > -
{i18n.DELETE_WARNING}
+
{warning}
); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.test.tsx index 18a6ffc06941c..cfbc7d255062f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.test.tsx @@ -7,8 +7,18 @@ import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; +import { useParams } from 'react-router-dom'; import { DeleteTimelineModalOverlay } from '.'; +import { TimelineType } from '../../../../../common/types/timeline'; + +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { + ...actual, + useParams: jest.fn(), + }; +}); describe('DeleteTimelineModal', () => { const savedObjectId = 'abcd'; @@ -20,6 +30,10 @@ describe('DeleteTimelineModal', () => { title: 'Privilege Escalation', }; + beforeAll(() => { + (useParams as jest.Mock).mockReturnValue({ tabName: TimelineType.default }); + }); + describe('showModalState', () => { test('it does NOT render the modal when isModalOpen is false', () => { const testProps = { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx index 8e754b3d04654..0c611ca5106e8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx @@ -5,12 +5,10 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep } from 'lodash/fp'; import moment from 'moment'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; -import { ThemeProvider } from 'styled-components'; import '../../../../common/mock/formatted_relative'; import { mockTimelineResults } from '../../../../common/mock/timeline_results'; @@ -18,7 +16,6 @@ import { OpenTimelineResult, TimelineResultNote } from '../types'; import { NotePreviews } from '.'; describe('NotePreviews', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); let mockResults: OpenTimelineResult[]; let note1updated: number; let note2updated: number; @@ -34,11 +31,7 @@ describe('NotePreviews', () => { test('it renders a note preview for each note when isModal is false', () => { const hasNotes: OpenTimelineResult[] = [{ ...mockResults[0] }]; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); hasNotes[0].notes!.forEach(({ savedObjectId }) => { expect(wrapper.find(`[data-test-subj="note-preview-${savedObjectId}"]`).exists()).toBe(true); @@ -48,11 +41,7 @@ describe('NotePreviews', () => { test('it renders a note preview for each note when isModal is true', () => { const hasNotes: OpenTimelineResult[] = [{ ...mockResults[0] }]; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); hasNotes[0].notes!.forEach(({ savedObjectId }) => { expect(wrapper.find(`[data-test-subj="note-preview-${savedObjectId}"]`).exists()).toBe(true); @@ -99,11 +88,7 @@ describe('NotePreviews', () => { }, ]; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); expect(wrapper.find('.euiCommentEvent__headerUsername').at(1).text()).toEqual('bob'); }); @@ -130,11 +115,7 @@ describe('NotePreviews', () => { }, ]; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); expect(wrapper.find(`.euiCommentEvent__headerUsername`).at(2).text()).toEqual('bob'); }); @@ -160,11 +141,7 @@ describe('NotePreviews', () => { }, ]; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); expect(wrapper.find(`.euiCommentEvent__headerUsername`).at(2).text()).toEqual('bob'); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx index 1aac1f21f2d50..0cf7f2891dfbf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep } from 'lodash/fp'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; @@ -32,8 +31,19 @@ jest.mock('react-router-dom', () => { }; }); +const mockTheme = { + eui: { + euiSizeL: '10px', + paddingSizes: { + s: '10px', + }, + euiBreakpoints: { + l: '1200px', + }, + }, +}; + describe('OpenTimeline', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); const title = 'All Timelines / Open Timelines'; let mockResults: OpenTimelineResult[]; @@ -73,7 +83,7 @@ describe('OpenTimeline', () => { test('it renders the search row', () => { const defaultProps = getDefaultTestProps(mockResults); const wrapper = mountWithIntl( - + ); @@ -84,7 +94,7 @@ describe('OpenTimeline', () => { test('it renders the timelines table', () => { const defaultProps = getDefaultTestProps(mockResults); const wrapper = mountWithIntl( - + ); @@ -95,7 +105,7 @@ describe('OpenTimeline', () => { test('it shows the delete action columns when onDeleteSelected and deleteTimelines are specified', () => { const defaultProps = getDefaultTestProps(mockResults); const wrapper = mountWithIntl( - + ); @@ -115,7 +125,7 @@ describe('OpenTimeline', () => { deleteTimelines: undefined, }; const wrapper = mountWithIntl( - + ); @@ -135,7 +145,7 @@ describe('OpenTimeline', () => { deleteTimelines: undefined, }; const wrapper = mountWithIntl( - + ); @@ -155,7 +165,7 @@ describe('OpenTimeline', () => { deleteTimelines: undefined, }; const wrapper = mountWithIntl( - + ); @@ -174,7 +184,7 @@ describe('OpenTimeline', () => { query: '', }; const wrapper = mountWithIntl( - + ); @@ -188,7 +198,7 @@ describe('OpenTimeline', () => { query: ' ', }; const wrapper = mountWithIntl( - + ); @@ -202,7 +212,7 @@ describe('OpenTimeline', () => { query: 'Would you like to go to Denver?', }; const wrapper = mountWithIntl( - + ); @@ -218,7 +228,7 @@ describe('OpenTimeline', () => { query: ' Is it starting to feel cramped in here? ', }; const wrapper = mountWithIntl( - + ); @@ -234,7 +244,7 @@ describe('OpenTimeline', () => { query: '', }; const wrapper = mountWithIntl( - + ); @@ -250,7 +260,7 @@ describe('OpenTimeline', () => { query: ' ', }; const wrapper = mountWithIntl( - + ); @@ -266,7 +276,7 @@ describe('OpenTimeline', () => { query: 'How was your day?', }; const wrapper = mountWithIntl( - + ); @@ -282,7 +292,7 @@ describe('OpenTimeline', () => { timelineStatus: TimelineStatus.active, }; const wrapper = mountWithIntl( - + ); @@ -297,7 +307,7 @@ describe('OpenTimeline', () => { selectedItems: [], }; const wrapper = mountWithIntl( - + ); @@ -317,7 +327,7 @@ describe('OpenTimeline', () => { selectedItems: [], }; const wrapper = mountWithIntl( - + ); @@ -337,7 +347,7 @@ describe('OpenTimeline', () => { selectedItems: [{}], }; const wrapper = mountWithIntl( - + ); @@ -357,7 +367,7 @@ describe('OpenTimeline', () => { selectedItems: [{}], }; const wrapper = mountWithIntl( - + ); @@ -376,7 +386,7 @@ describe('OpenTimeline', () => { timelineStatus: TimelineStatus.active, }; const wrapper = mountWithIntl( - + ); @@ -392,7 +402,7 @@ describe('OpenTimeline', () => { timelineStatus: TimelineStatus.active, }; const wrapper = mountWithIntl( - + ); @@ -406,7 +416,7 @@ describe('OpenTimeline', () => { timelineStatus: TimelineStatus.immutable, }; const wrapper = mountWithIntl( - + ); @@ -420,7 +430,7 @@ describe('OpenTimeline', () => { timelineStatus: TimelineStatus.immutable, }; const wrapper = mountWithIntl( - + ); @@ -436,7 +446,7 @@ describe('OpenTimeline', () => { timelineStatus: TimelineStatus.immutable, }; const wrapper = mountWithIntl( - + ); @@ -450,7 +460,7 @@ describe('OpenTimeline', () => { timelineStatus: null, }; const wrapper = mountWithIntl( - + ); @@ -464,7 +474,7 @@ describe('OpenTimeline', () => { timelineStatus: null, }; const wrapper = mountWithIntl( - + ); @@ -480,7 +490,7 @@ describe('OpenTimeline', () => { timelineStatus: null, }; const wrapper = mountWithIntl( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx index 24b0702770d3c..5a3da748bea1d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx @@ -122,9 +122,9 @@ export const OpenTimeline = React.memo( const onRefreshBtnClick = useCallback(() => { if (refetch != null) { - refetch(searchResults, totalSearchResultsCount); + refetch(); } - }, [refetch, searchResults, totalSearchResultsCount]); + }, [refetch]); const handleCloseModal = useCallback(() => { if (setImportDataModalToggle != null) { @@ -137,9 +137,9 @@ export const OpenTimeline = React.memo( setImportDataModalToggle(false); } if (refetch != null) { - refetch(searchResults, totalSearchResultsCount); + refetch(); } - }, [setImportDataModalToggle, refetch, searchResults, totalSearchResultsCount]); + }, [setImportDataModalToggle, refetch]); const actionTimelineToShow = useMemo(() => { const timelineActions: ActionTimelineToShow[] = ['createFrom', 'duplicate']; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx index 5babecb3acb69..38186d35d2d2d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep } from 'lodash/fp'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; @@ -23,7 +22,14 @@ import { TimelineType, TimelineStatus } from '../../../../../common/types/timeli jest.mock('../../../../common/lib/kibana'); describe('OpenTimelineModal', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); + const mockTheme = { + eui: { + euiColorMediumShade: '#ece', + euiBreakpoints: { + s: '500px', + }, + }, + }; const title = 'All Timelines / Open Timelines'; let mockResults: OpenTimelineResult[]; @@ -62,7 +68,7 @@ describe('OpenTimelineModal', () => { test('it renders the title row', () => { const defaultProps = getDefaultTestProps(mockResults); const wrapper = mountWithIntl( - + ); @@ -73,7 +79,7 @@ describe('OpenTimelineModal', () => { test('it renders the search row', () => { const defaultProps = getDefaultTestProps(mockResults); const wrapper = mountWithIntl( - + ); @@ -84,7 +90,7 @@ describe('OpenTimelineModal', () => { test('it renders the timelines table', () => { const defaultProps = getDefaultTestProps(mockResults); const wrapper = mountWithIntl( - + ); @@ -99,7 +105,7 @@ describe('OpenTimelineModal', () => { deleteTimelines: jest.fn(), }; const wrapper = mountWithIntl( - + ); @@ -119,7 +125,7 @@ describe('OpenTimelineModal', () => { deleteTimelines: undefined, }; const wrapper = mountWithIntl( - + ); @@ -139,7 +145,7 @@ describe('OpenTimelineModal', () => { deleteTimelines: undefined, }; const wrapper = mountWithIntl( - + ); @@ -159,7 +165,7 @@ describe('OpenTimelineModal', () => { deleteTimelines: undefined, }; const wrapper = mountWithIntl( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx index 837dcbe1d6bfd..62cdda6070b32 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx @@ -5,11 +5,9 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount } from 'enzyme'; import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; -import { ThemeProvider } from 'styled-components'; import { waitFor } from '@testing-library/react'; import { TestProviders } from '../../../../common/mock/test_providers'; @@ -19,8 +17,6 @@ import * as i18n from '../translations'; import { OpenTimelineModalButton } from './open_timeline_modal_button'; describe('OpenTimelineModalButton', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - test('it renders the expected button text', async () => { const wrapper = mount( @@ -43,13 +39,11 @@ describe('OpenTimelineModalButton', () => { test('it invokes onClick function provided as a prop when the button is clicked', async () => { const onClick = jest.fn(); const wrapper = mount( - - - - - - - + + + + + ); await waitFor(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.test.tsx index e0b252e112fc6..d75823b771681 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.test.tsx @@ -6,7 +6,6 @@ */ import { EuiFilterButtonProps } from '@elastic/eui'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; import { ThemeProvider } from 'styled-components'; @@ -17,12 +16,16 @@ import { SearchRow } from '.'; import * as i18n from '../translations'; -describe('SearchRow', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); +const mockTheme = { + eui: { + euiSizeL: '10px', + }, +}; +describe('SearchRow', () => { test('it renders a search input with the expected placeholder when the query is empty', () => { const wrapper = mountWithIntl( - + { describe('Only Favorites Button', () => { test('it renders the expected button text', () => { const wrapper = mountWithIntl( - + { const onToggleOnlyFavorites = jest.fn(); const wrapper = mountWithIntl( - + { test('it sets the button to the toggled state when onlyFavorites is true', () => { const wrapper = mountWithIntl( - + { test('it sets the button to the NON-toggled state when onlyFavorites is false', () => { const wrapper = mountWithIntl( - + { test('it invokes onQueryChange when the user enters a query', () => { const wrapper = mountWithIntl( - + { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); let mockResults: OpenTimelineResult[]; beforeEach(() => { @@ -37,7 +37,7 @@ describe('#getActionsColumns', () => { actionTimelineToShow: ['delete'], }; const wrapper = mountWithIntl( - + ); @@ -51,7 +51,7 @@ describe('#getActionsColumns', () => { actionTimelineToShow: [], }; const wrapper = mountWithIntl( - + ); @@ -65,7 +65,7 @@ describe('#getActionsColumns', () => { actionTimelineToShow: ['duplicate'], }; const wrapper = mountWithIntl( - + ); @@ -79,7 +79,7 @@ describe('#getActionsColumns', () => { actionTimelineToShow: ['duplicate'], }; const wrapper = mountWithIntl( - + ); @@ -93,7 +93,7 @@ describe('#getActionsColumns', () => { actionTimelineToShow: [], }; const wrapper = mountWithIntl( - + ); @@ -107,7 +107,7 @@ describe('#getActionsColumns', () => { actionTimelineToShow: ['delete'], }; const wrapper = mountWithIntl( - + ); @@ -124,7 +124,7 @@ describe('#getActionsColumns', () => { ...getMockTimelinesTableProps(missingSavedObjectId), }; const wrapper = mountWithIntl( - + ); @@ -141,7 +141,7 @@ describe('#getActionsColumns', () => { ...getMockTimelinesTableProps(mockResults), }; const wrapper = mountWithIntl( - + ); @@ -161,7 +161,7 @@ describe('#getActionsColumns', () => { onOpenTimeline, }; const wrapper = mountWithIntl( - + ); @@ -177,7 +177,7 @@ describe('#getActionsColumns', () => { actionTimelineToShow: ['export'], }; const wrapper = mountWithIntl( - + ); @@ -189,7 +189,7 @@ describe('#getActionsColumns', () => { ...getMockTimelinesTableProps(mockResults), }; const wrapper = mountWithIntl( - + ); @@ -206,7 +206,7 @@ describe('#getActionsColumns', () => { actionTimelineToShow: ['export'], }; const wrapper = mountWithIntl( - + ); @@ -226,7 +226,7 @@ describe('#getActionsColumns', () => { enableExportTimelineDownloader, }; const wrapper = mountWithIntl( - + ); @@ -242,7 +242,7 @@ describe('#getActionsColumns', () => { actionTimelineToShow: ['createFrom', 'duplicate'], }; const wrapper = mountWithIntl( - + ); @@ -256,7 +256,7 @@ describe('#getActionsColumns', () => { actionTimelineToShow: ['createFrom', 'duplicate'], }; const wrapper = mountWithIntl( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx index 3108dd09ea687..3c70cc70a66de 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx @@ -6,7 +6,6 @@ */ import { EuiButtonIconProps } from '@elastic/eui'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep, omit } from 'lodash/fp'; import React from 'react'; import { ThemeProvider } from 'styled-components'; @@ -23,10 +22,11 @@ import { TimelinesTable, TimelinesTableProps } from '.'; import * as i18n from '../translations'; import { getMockTimelinesTableProps } from './mocks'; +const mockTheme = { eui: { euiColorMediumShade: '#ece' } }; + jest.mock('../../../../common/lib/kibana'); describe('#getCommonColumns', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); let mockResults: OpenTimelineResult[]; beforeEach(() => { @@ -40,11 +40,7 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(hasNotes), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); expect(wrapper.find('[data-test-subj="expand-notes"]').exists()).toBe(true); }); @@ -55,11 +51,7 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(missingNotes), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); expect(wrapper.find('[data-test-subj="expand-notes"]').exists()).toBe(false); }); @@ -70,11 +62,7 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(nullNotes), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); expect(wrapper.find('[data-test-subj="expand-notes"]').exists()).toBe(false); }); @@ -85,11 +73,7 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(emptylNotes), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); expect(wrapper.find('[data-test-subj="expand-notes"]').exists()).toBe(false); }); @@ -101,11 +85,7 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(missingSavedObjectId), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); expect(wrapper.find('[data-test-subj="expand-notes"]').exists()).toBe(false); }); @@ -116,11 +96,7 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(nullSavedObjectId), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); expect(wrapper.find('[data-test-subj="expand-notes"]').exists()).toBe(false); }); @@ -131,11 +107,7 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(hasNotes), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); const props = wrapper .find('[data-test-subj="expand-notes"]') @@ -156,11 +128,7 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(hasNotes), itemIdToExpandedNotesRowMap, }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); const props = wrapper .find('[data-test-subj="expand-notes"]') @@ -184,11 +152,7 @@ describe('#getCommonColumns', () => { itemIdToExpandedNotesRowMap, onToggleShowNotes, }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); wrapper.find('[data-test-subj="expand-notes"]').first().simulate('click'); @@ -214,7 +178,7 @@ describe('#getCommonColumns', () => { onToggleShowNotes, }; const wrapper = mountWithIntl( - + ); @@ -233,7 +197,7 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(mockResults), }; const wrapper = mountWithIntl( - + ); @@ -246,7 +210,7 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(mockResults), }; const wrapper = mountWithIntl( - + ); @@ -265,7 +229,7 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(missingSavedObjectId), }; const wrapper = mountWithIntl( - + ); @@ -285,7 +249,7 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(missingTitle), }; const wrapper = mountWithIntl( - + ); @@ -304,7 +268,7 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(withMissingSavedObjectIdAndTitle), }; const wrapper = mountWithIntl( - + ); @@ -323,7 +287,7 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(withJustWhitespaceTitle), }; const wrapper = mountWithIntl( - + ); @@ -345,7 +309,7 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(withMissingSavedObjectId), }; const wrapper = mountWithIntl( - + ); @@ -360,7 +324,7 @@ describe('#getCommonColumns', () => { test('it renders a hyperlink when the timeline has a saved object id', () => { const wrapper = mountWithIntl( - + ); @@ -379,7 +343,7 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(missingSavedObjectId), }; const wrapper = mountWithIntl( - + ); @@ -397,7 +361,7 @@ describe('#getCommonColumns', () => { onOpenTimeline, }; const wrapper = mountWithIntl( - + ); @@ -417,7 +381,7 @@ describe('#getCommonColumns', () => { describe('Description column', () => { test('it renders the expected column name', () => { const wrapper = mountWithIntl( - + ); @@ -427,7 +391,7 @@ describe('#getCommonColumns', () => { test('it renders the description when the timeline has a description', () => { const wrapper = mountWithIntl( - + ); @@ -441,7 +405,7 @@ describe('#getCommonColumns', () => { const missingDescription: OpenTimelineResult[] = [omit('description', { ...mockResults[0] })]; const wrapper = mountWithIntl( - + ); @@ -459,7 +423,7 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(justWhitespaceDescription), }; const wrapper = mountWithIntl( - + ); @@ -472,7 +436,7 @@ describe('#getCommonColumns', () => { describe('Last Modified column', () => { test('it renders the expected column name', () => { const wrapper = mountWithIntl( - + ); @@ -482,7 +446,7 @@ describe('#getCommonColumns', () => { test('it renders the last modified (updated) date when the timeline has an updated property', () => { const wrapper = mountWithIntl( - + ); @@ -497,7 +461,7 @@ describe('#getCommonColumns', () => { const missingUpdated: OpenTimelineResult[] = [omit('updated', { ...mockResults[0] })]; const wrapper = mountWithIntl( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/extended_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/extended_columns.test.tsx index 66556296c42ac..83e21267bce28 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/extended_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/extended_columns.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep, omit } from 'lodash/fp'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; @@ -21,10 +20,11 @@ import { TimelinesTable, TimelinesTableProps } from '.'; import * as i18n from '../translations'; import { getMockTimelinesTableProps } from './mocks'; +const mockTheme = { eui: { euiColorMediumShade: '#ece' } }; + jest.mock('../../../../common/lib/kibana'); describe('#getExtendedColumns', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); let mockResults: OpenTimelineResult[]; beforeEach(() => { @@ -37,7 +37,7 @@ describe('#getExtendedColumns', () => { ...getMockTimelinesTableProps(mockResults), }; const wrapper = mountWithIntl( - + ); @@ -50,7 +50,7 @@ describe('#getExtendedColumns', () => { ...getMockTimelinesTableProps(mockResults), }; const wrapper = mountWithIntl( - + ); @@ -66,7 +66,7 @@ describe('#getExtendedColumns', () => { ...getMockTimelinesTableProps(missingUpdatedBy), }; const wrapper = mountWithIntl( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/icon_header_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/icon_header_columns.test.tsx index c3681753c7732..a8ed5f02fa3ef 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/icon_header_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/icon_header_columns.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep, omit } from 'lodash/fp'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; @@ -16,10 +15,12 @@ import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { TimelinesTable, TimelinesTableProps } from '.'; import { OpenTimelineResult } from '../types'; import { getMockTimelinesTableProps } from './mocks'; + +const mockTheme = { eui: { euiColorMediumShade: '#ece' } }; + jest.mock('../../../../common/lib/kibana'); describe('#getActionsColumns', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); let mockResults: OpenTimelineResult[]; beforeEach(() => { @@ -28,7 +29,7 @@ describe('#getActionsColumns', () => { test('it renders the pinned events header icon', () => { const wrapper = mountWithIntl( - + ); @@ -42,7 +43,7 @@ describe('#getActionsColumns', () => { ...getMockTimelinesTableProps(with6Events), }; const wrapper = mountWithIntl( - + ); @@ -52,7 +53,7 @@ describe('#getActionsColumns', () => { test('it renders the notes count header icon', () => { const wrapper = mountWithIntl( - + ); @@ -66,7 +67,7 @@ describe('#getActionsColumns', () => { ...getMockTimelinesTableProps(with4Notes), }; const wrapper = mountWithIntl( - + ); @@ -76,7 +77,7 @@ describe('#getActionsColumns', () => { test('it renders the favorites header icon', () => { const wrapper = mountWithIntl( - + ); @@ -90,7 +91,7 @@ describe('#getActionsColumns', () => { ...getMockTimelinesTableProps(undefinedFavorite), }; const wrapper = mountWithIntl( - + ); @@ -104,7 +105,7 @@ describe('#getActionsColumns', () => { ...getMockTimelinesTableProps(nullFavorite), }; const wrapper = mountWithIntl( - + ); @@ -118,7 +119,7 @@ describe('#getActionsColumns', () => { ...getMockTimelinesTableProps(emptyFavorite), }; const wrapper = mountWithIntl( - + ); @@ -143,7 +144,7 @@ describe('#getActionsColumns', () => { ...getMockTimelinesTableProps(favorite), }; const wrapper = mountWithIntl( - + ); @@ -172,7 +173,7 @@ describe('#getActionsColumns', () => { ...getMockTimelinesTableProps(favorite), }; const wrapper = mountWithIntl( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.test.tsx index 2d5949ae41125..01a855524ac0d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep } from 'lodash/fp'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; @@ -19,10 +18,11 @@ import { getMockTimelinesTableProps } from './mocks'; import * as i18n from '../translations'; +const mockTheme = { eui: { euiColorMediumShade: '#ece' } }; + jest.mock('../../../../common/lib/kibana'); describe('TimelinesTable', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); let mockResults: OpenTimelineResult[]; beforeEach(() => { @@ -31,7 +31,7 @@ describe('TimelinesTable', () => { test('it renders the select all timelines header checkbox when actionTimelineToShow has the action selectable', () => { const wrapper = mountWithIntl( - + ); @@ -45,7 +45,7 @@ describe('TimelinesTable', () => { actionTimelineToShow: ['delete', 'duplicate'], }; const wrapper = mountWithIntl( - + ); @@ -59,7 +59,7 @@ describe('TimelinesTable', () => { showExtendedColumns: true, }; const wrapper = mountWithIntl( - + ); @@ -73,7 +73,7 @@ describe('TimelinesTable', () => { showExtendedColumns: false, }; const wrapper = mountWithIntl( - + ); @@ -90,7 +90,7 @@ describe('TimelinesTable', () => { test('it renders the delete timeline (trash icon) when actionTimelineToShow has the delete action', () => { const wrapper = mountWithIntl( - + ); @@ -104,7 +104,7 @@ describe('TimelinesTable', () => { actionTimelineToShow: ['duplicate', 'selectable'], }; const wrapper = mountWithIntl( - + ); @@ -114,7 +114,7 @@ describe('TimelinesTable', () => { test('it renders the rows per page selector when showExtendedColumns is true', () => { const wrapper = mountWithIntl( - + ); @@ -128,7 +128,7 @@ describe('TimelinesTable', () => { showExtendedColumns: false, }; const wrapper = mountWithIntl( - + ); @@ -144,7 +144,7 @@ describe('TimelinesTable', () => { pageSize: defaultPageSize, }; const wrapper = mountWithIntl( - + ); @@ -156,7 +156,7 @@ describe('TimelinesTable', () => { test('it sorts the Last Modified column in descending order when showExtendedColumns is true ', () => { const wrapper = mountWithIntl( - + ); @@ -170,7 +170,7 @@ describe('TimelinesTable', () => { showExtendedColumns: false, }; const wrapper = mountWithIntl( - + ); @@ -184,7 +184,7 @@ describe('TimelinesTable', () => { searchResults: [], }; const wrapper = mountWithIntl( - + ); @@ -199,7 +199,7 @@ describe('TimelinesTable', () => { onTableChange, }; const wrapper = mountWithIntl( - + ); @@ -221,7 +221,7 @@ describe('TimelinesTable', () => { onSelectionChange, }; const wrapper = mountWithIntl( - + ); @@ -242,7 +242,7 @@ describe('TimelinesTable', () => { loading: true, }; const wrapper = mountWithIntl( - + ); @@ -257,7 +257,7 @@ describe('TimelinesTable', () => { test('it disables the table loading animation when isLoading is false', () => { const wrapper = mountWithIntl( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx index 8c553bb95e9bd..c1b30f3e68cf4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx @@ -25,7 +25,11 @@ import { getActionsColumns } from './actions_columns'; import { getCommonColumns } from './common_columns'; import { getExtendedColumns } from './extended_columns'; import { getIconHeaderColumns } from './icon_header_columns'; -import { TimelineTypeLiteralWithNull, TimelineStatus } from '../../../../../common/types/timeline'; +import { + TimelineTypeLiteralWithNull, + TimelineStatus, + TimelineType, +} from '../../../../../common/types/timeline'; // there are a number of type mismatches across this file const EuiBasicTable: any = _EuiBasicTable; // eslint-disable-line @typescript-eslint/no-explicit-any @@ -103,7 +107,7 @@ export interface TimelinesTableProps { onToggleShowNotes: OnToggleShowNotes; pageIndex: number; pageSize: number; - searchResults: OpenTimelineResult[]; + searchResults: OpenTimelineResult[] | null; showExtendedColumns: boolean; sortDirection: 'asc' | 'desc'; sortField: string; @@ -196,6 +200,13 @@ export const TimelinesTable = React.memo( ] ); + const noItemsMessage = + isLoading || searchResults == null + ? i18n.LOADING + : timelineType === TimelineType.template + ? i18n.ZERO_TIMELINE_TEMPLATES_MATCH + : i18n.ZERO_TIMELINES_MATCH; + return ( ( isSelectable={actionTimelineToShow.includes('selectable')} itemId="savedObjectId" itemIdToExpandedRowMap={itemIdToExpandedNotesRowMap} - items={searchResults} + items={searchResults ?? []} loading={isLoading} - noItemsMessage={i18n.ZERO_TIMELINES_MATCH} + noItemsMessage={noItemsMessage} onChange={onTableChange} pagination={pagination} selection={actionTimelineToShow.includes('selectable') ? selection : undefined} diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/title_row/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/title_row/index.test.tsx index 5621a2287f3a2..4661f72901eb6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/title_row/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/title_row/index.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { EuiButtonProps } from '@elastic/eui'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; @@ -13,13 +12,16 @@ import { ThemeProvider } from 'styled-components'; import { TitleRow } from '.'; +const mockTheme = { + eui: { euiSizeS: '10px', euiLineHeight: '20px', euiBreakpoints: { s: '10px' }, euiSize: '10px' }, +}; + describe('TitleRow', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); const title = 'All Timelines / Open Timelines'; test('it renders the title', () => { const wrapper = mountWithIntl( - + ); @@ -30,7 +32,7 @@ describe('TitleRow', () => { describe('Favorite Selected button', () => { test('it renders the Favorite Selected button when onAddTimelinesToFavorites is provided', () => { const wrapper = mountWithIntl( - + { test('it does NOT render the Favorite Selected button when onAddTimelinesToFavorites is NOT provided', () => { const wrapper = mountWithIntl( - + ); @@ -54,7 +56,7 @@ describe('TitleRow', () => { test('it disables the Favorite Selected button when the selectedTimelinesCount is 0', () => { const wrapper = mountWithIntl( - + { test('it enables the Favorite Selected button when the selectedTimelinesCount is greater than 0', () => { const wrapper = mountWithIntl( - + { const onAddTimelinesToFavorites = jest.fn(); const wrapper = mountWithIntl( - + export const IMPORT_FAILED = i18n.translate( 'xpack.securitySolution.timelines.components.importTimelineModal.importFailedTitle', { - defaultMessage: 'Failed to import timelines', + defaultMessage: 'Failed to import', } ); export const IMPORT_TIMELINE = i18n.translate( 'xpack.securitySolution.timelines.components.importTimelineModal.importTitle', { - defaultMessage: 'Import timeline…', + defaultMessage: 'Import…', } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index ad62bda4c9783..47e1da2d240ea 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts @@ -167,9 +167,9 @@ export interface OpenTimelineProps { /** The currently applied search criteria */ query: string; /** Refetch table */ - refetch?: (existingTimeline?: OpenTimelineResult[], existingCount?: number) => void; - /** The results of executing a search */ - searchResults: OpenTimelineResult[]; + refetch?: () => void; + /** The results of executing a search, null is the status before data fatched */ + searchResults: OpenTimelineResult[] | null; /** the currently-selected timelines in the table */ selectedItems: OpenTimelineResult[]; /** Toggle export timelines modal*/ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.test.tsx index 88521b779925a..666fb254aaa2c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.test.tsx @@ -5,11 +5,9 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, shallow } from 'enzyme'; import { cloneDeep } from 'lodash'; import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mockBrowserFields } from '../../../../../common/containers/source/mock'; import { Ecs } from '../../../../../../common/ecs'; @@ -42,11 +40,7 @@ describe('plain_row_renderer', () => { data: mockDatum, timelineId: 'test', }); - const wrapper = mount( - ({ eui: euiDarkVars, darkMode: true })}> - {children} - - ); + const wrapper = mount({children}); expect(wrapper.text()).toEqual(''); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/unknown_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/unknown_column_renderer.test.tsx index d98a724ebf9cb..6d7a9e5aecfd9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/unknown_column_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/unknown_column_renderer.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, shallow } from 'enzyme'; import { cloneDeep } from 'lodash'; import React from 'react'; @@ -17,8 +16,13 @@ import { getEmptyValue } from '../../../../../common/components/empty_value'; import { unknownColumnRenderer } from './unknown_column_renderer'; import { getValues } from './helpers'; +const mockTheme = { + eui: { + euiColorMediumShade: '#ece', + }, +}; + describe('unknown_column_renderer', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); let mockDatum: TimelineNonEcsData[]; const _id = mockTimelineData[0]._id; beforeEach(() => { @@ -50,7 +54,7 @@ describe('unknown_column_renderer', () => { timelineId: 'test', }); const wrapper = mount( - + {emptyColumn} ); @@ -66,7 +70,7 @@ describe('unknown_column_renderer', () => { timelineId: 'test', }); const wrapper = mount( - + {emptyColumn} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx index d4de77e04e9f7..7ccce80bbe9a4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx @@ -222,7 +222,7 @@ const SelectableTimelineComponent: React.FC = ({ windowProps: { onScroll: ({ scrollOffset }) => handleOnScroll( - timelines.filter((t) => !hideUntitled || t.title !== '').length, + (timelines ?? []).filter((t) => !hideUntitled || t.title !== '').length, timelineCount, scrollOffset ), @@ -254,7 +254,7 @@ const SelectableTimelineComponent: React.FC = ({ = ({ searchProps={searchProps} singleSelection={true} options={getSelectableOptions({ - timelines, + timelines: timelines ?? [], onlyFavorites, searchTimelineValue, timelineType, diff --git a/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx index b14ccbd319399..82b41a95bd537 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx @@ -38,7 +38,7 @@ export interface AllTimelinesArgs { status, timelineType, }: AllTimelinesVariables) => void; - timelines: OpenTimelineResult[]; + timelines: OpenTimelineResult[] | null; loading: boolean; totalCount: number; customTemplateTimelineCount: number; @@ -105,7 +105,7 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { const [allTimelines, setAllTimelines] = useState>({ loading: false, totalCount: 0, - timelines: [], + timelines: null, // use null as initial state to distinguish between empty result and haven't started loading. customTemplateTimelineCount: 0, defaultTimelineCount: 0, elasticTemplateTimelineCount: 0, @@ -128,7 +128,10 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { const fetchData = async () => { try { if (apolloClient != null) { - setAllTimelines((prevState) => ({ ...prevState, loading: true })); + setAllTimelines((prevState) => ({ + ...prevState, + loading: true, + })); const variables: GetAllTimeline.Variables = { onlyUserFavorite, diff --git a/x-pack/plugins/security_solution/public/timelines/pages/index.tsx b/x-pack/plugins/security_solution/public/timelines/pages/index.tsx index 53ea28832f47f..806ac57df1f65 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/index.tsx @@ -12,7 +12,6 @@ import { Switch, Route, useHistory } from 'react-router-dom'; import { ChromeBreadcrumb } from '../../../../../../src/core/public'; import { TimelineType } from '../../../common/types/timeline'; -import { TAB_TIMELINES, TAB_TEMPLATES } from '../components/open_timeline/translations'; import { TimelineRouteSpyState } from '../../common/utils/route/types'; import { TimelinesPage } from './timelines_page'; @@ -25,37 +24,18 @@ import { SecurityPageName } from '../../app/types'; const timelinesPagePath = `/:tabName(${TimelineType.default}|${TimelineType.template})`; const timelinesDefaultPath = `/${TimelineType.default}`; -const TabNameMappedToI18nKey: Record = { - [TimelineType.default]: TAB_TIMELINES, - [TimelineType.template]: TAB_TEMPLATES, -}; - export const getBreadcrumbs = ( params: TimelineRouteSpyState, search: string[], getUrlForApp: GetUrlForApp -): ChromeBreadcrumb[] => { - let breadcrumb = [ - { - text: PAGE_TITLE, - href: getUrlForApp(`${APP_ID}:${SecurityPageName.timelines}`, { - path: !isEmpty(search[0]) ? search[0] : '', - }), - }, - ]; - - const tabName = params?.tabName; - if (!tabName) return breadcrumb; - - breadcrumb = [ - ...breadcrumb, - { - text: TabNameMappedToI18nKey[tabName], - href: '', - }, - ]; - return breadcrumb; -}; +): ChromeBreadcrumb[] => [ + { + text: PAGE_TITLE, + href: getUrlForApp(`${APP_ID}:${SecurityPageName.timelines}`, { + path: !isEmpty(search[0]) ? search[0] : '', + }), + }, +]; export const Timelines = React.memo(() => { const history = useHistory(); diff --git a/x-pack/plugins/security_solution/public/timelines/pages/translations.ts b/x-pack/plugins/security_solution/public/timelines/pages/translations.ts index f3bff98785619..199fc27c2663a 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/pages/translations.ts @@ -21,7 +21,7 @@ export const ALL_TIMELINES_PANEL_TITLE = i18n.translate( export const ALL_TIMELINES_IMPORT_TIMELINE_TITLE = i18n.translate( 'xpack.securitySolution.timelines.allTimelines.importTimelineTitle', { - defaultMessage: 'Import Timeline', + defaultMessage: 'Import', } ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 55d128225c555..50823ebd85d05 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -5,7 +5,6 @@ * 2.0. */ -import moment from 'moment'; import { sampleRuleAlertParams, sampleEmptyDocSearchResults, @@ -23,9 +22,10 @@ import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mock import uuid from 'uuid'; import { listMock } from '../../../../../lists/server/mocks'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { BulkResponse } from './types'; +import { BulkResponse, RuleRangeTuple } from './types'; import { SearchListItemArraySchema } from '../../../../../lists/common/schemas'; import { getSearchListItemResponseMock } from '../../../../../lists/common/schemas/response/search_list_item_schema.mock'; +import { getRuleRangeTuples } from './utils'; const buildRuleMessage = buildRuleMessageFactory({ id: 'fake id', @@ -39,16 +39,26 @@ describe('searchAfterAndBulkCreate', () => { let inputIndexPattern: string[] = []; let listClient = listMock.getListClient(); const someGuids = Array.from({ length: 13 }).map(() => uuid.v4()); + const sampleParams = sampleRuleAlertParams(30); + let tuples: RuleRangeTuple[]; beforeEach(() => { jest.clearAllMocks(); listClient = listMock.getListClient(); listClient.searchListItemByValues = jest.fn().mockResolvedValue([]); inputIndexPattern = ['auditbeat-*']; mockService = alertsMock.createAlertServices(); + ({ tuples } = getRuleRangeTuples({ + logger: mockLogger, + previousStartedAt: new Date(), + from: sampleParams.from, + to: sampleParams.to, + interval: '5m', + maxSignals: sampleParams.maxSignals, + buildRuleMessage, + })); }); test('should return success with number of searches less than max signals', async () => { - const sampleParams = sampleRuleAlertParams(30); mockService.callCluster .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) .mockResolvedValueOnce({ @@ -112,11 +122,9 @@ describe('searchAfterAndBulkCreate', () => { }, }, ]; - const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - gap: null, - previousStartedAt: new Date(), + tuples, listClient, exceptionsList: [exceptionItem], services: mockService, @@ -147,7 +155,6 @@ describe('searchAfterAndBulkCreate', () => { }); test('should return success with number of searches less than max signals with gap', async () => { - const sampleParams = sampleRuleAlertParams(30); mockService.callCluster .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) .mockResolvedValueOnce({ @@ -201,8 +208,7 @@ describe('searchAfterAndBulkCreate', () => { ]; const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - gap: moment.duration(2, 'm'), - previousStartedAt: moment().subtract(10, 'm').toDate(), + tuples, listClient, exceptionsList: [exceptionItem], services: mockService, @@ -233,7 +239,6 @@ describe('searchAfterAndBulkCreate', () => { }); test('should return success when no search results are in the allowlist', async () => { - const sampleParams = sampleRuleAlertParams(30); mockService.callCluster .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3))) .mockResolvedValueOnce({ @@ -278,8 +283,7 @@ describe('searchAfterAndBulkCreate', () => { ]; const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - gap: null, - previousStartedAt: new Date(), + tuples, listClient, exceptionsList: [exceptionItem], services: mockService, @@ -305,7 +309,7 @@ describe('searchAfterAndBulkCreate', () => { }); expect(success).toEqual(true); expect(mockService.callCluster).toHaveBeenCalledTimes(3); - expect(createdSignalsCount).toEqual(4); // should not create any signals because all events were in the allowlist + expect(createdSignalsCount).toEqual(4); expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); @@ -316,7 +320,6 @@ describe('searchAfterAndBulkCreate', () => { { ...getSearchListItemResponseMock(), value: ['3.3.3.3'] }, ]; listClient.searchListItemByValues = jest.fn().mockResolvedValue(searchListItems); - const sampleParams = sampleRuleAlertParams(30); mockService.callCluster .mockResolvedValueOnce( repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ @@ -342,8 +345,7 @@ describe('searchAfterAndBulkCreate', () => { ]; const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - gap: null, - previousStartedAt: new Date(), + tuples, listClient, exceptionsList: [exceptionItem], services: mockService, @@ -382,7 +384,6 @@ describe('searchAfterAndBulkCreate', () => { ]; listClient.searchListItemByValues = jest.fn().mockResolvedValue(searchListItems); - const sampleParams = sampleRuleAlertParams(30); mockService.callCluster.mockResolvedValueOnce( repeatedSearchResultsWithNoSortId(4, 4, someGuids.slice(0, 3), [ '1.1.1.1', @@ -406,8 +407,7 @@ describe('searchAfterAndBulkCreate', () => { ]; const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - gap: null, - previousStartedAt: new Date(), + tuples, listClient, exceptionsList: [exceptionItem], services: mockService, @@ -437,13 +437,12 @@ describe('searchAfterAndBulkCreate', () => { expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); // I don't like testing log statements since logs change but this is the best // way I can think of to ensure this section is getting hit with this test case. - expect(((mockLogger.debug as unknown) as jest.Mock).mock.calls[8][0]).toContain( + expect(((mockLogger.debug as unknown) as jest.Mock).mock.calls[7][0]).toContain( 'ran out of sort ids to sort on name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); }); test('should return success when no sortId present but search results are in the allowlist', async () => { - const sampleParams = sampleRuleAlertParams(30); mockService.callCluster .mockResolvedValueOnce(repeatedSearchResultsWithNoSortId(4, 4, someGuids.slice(0, 3))) .mockResolvedValueOnce({ @@ -487,8 +486,7 @@ describe('searchAfterAndBulkCreate', () => { ]; const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - gap: null, - previousStartedAt: new Date(), + tuples, listClient, exceptionsList: [exceptionItem], services: mockService, @@ -514,17 +512,16 @@ describe('searchAfterAndBulkCreate', () => { }); expect(success).toEqual(true); expect(mockService.callCluster).toHaveBeenCalledTimes(2); - expect(createdSignalsCount).toEqual(4); // should not create any signals because all events were in the allowlist + expect(createdSignalsCount).toEqual(4); expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); // I don't like testing log statements since logs change but this is the best // way I can think of to ensure this section is getting hit with this test case. - expect(((mockLogger.debug as unknown) as jest.Mock).mock.calls[15][0]).toContain( + expect(((mockLogger.debug as unknown) as jest.Mock).mock.calls[14][0]).toContain( 'ran out of sort ids to sort on name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); }); test('should return success when no exceptions list provided', async () => { - const sampleParams = sampleRuleAlertParams(30); mockService.callCluster .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3))) .mockResolvedValueOnce({ @@ -565,8 +562,7 @@ describe('searchAfterAndBulkCreate', () => { ); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - gap: null, - previousStartedAt: new Date(), + tuples, listClient, exceptionsList: [], services: mockService, @@ -592,7 +588,7 @@ describe('searchAfterAndBulkCreate', () => { }); expect(success).toEqual(true); expect(mockService.callCluster).toHaveBeenCalledTimes(3); - expect(createdSignalsCount).toEqual(4); // should not create any signals because all events were in the allowlist + expect(createdSignalsCount).toEqual(4); expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); @@ -609,15 +605,13 @@ describe('searchAfterAndBulkCreate', () => { }, }, ]; - const sampleParams = sampleRuleAlertParams(10); mockService.callCluster .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) .mockRejectedValue(new Error('bulk failed')); // Added this recently const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ listClient, exceptionsList: [exceptionItem], - gap: null, - previousStartedAt: new Date(), + tuples, ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -659,7 +653,6 @@ describe('searchAfterAndBulkCreate', () => { }, }, ]; - const sampleParams = sampleRuleAlertParams(30); mockService.callCluster.mockResolvedValueOnce(sampleEmptyDocSearchResults()); listClient.searchListItemByValues = jest.fn(({ value }) => Promise.resolve( @@ -672,8 +665,7 @@ describe('searchAfterAndBulkCreate', () => { const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ listClient, exceptionsList: [exceptionItem], - gap: null, - previousStartedAt: new Date(), + tuples, ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -702,7 +694,6 @@ describe('searchAfterAndBulkCreate', () => { }); test('if returns false when singleSearchAfter throws an exception', async () => { - const sampleParams = sampleRuleAlertParams(10); mockService.callCluster .mockResolvedValueOnce({ took: 100, @@ -741,8 +732,7 @@ describe('searchAfterAndBulkCreate', () => { const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ listClient, exceptionsList: [exceptionItem], - gap: null, - previousStartedAt: new Date(), + tuples, ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -771,7 +761,6 @@ describe('searchAfterAndBulkCreate', () => { }); test('it returns error array when singleSearchAfter returns errors', async () => { - const sampleParams = sampleRuleAlertParams(30); const bulkItem: BulkResponse = { took: 100, errors: true, @@ -832,7 +821,6 @@ describe('searchAfterAndBulkCreate', () => { ], }) .mockResolvedValueOnce(sampleDocSearchResultsNoSortIdNoHits()); - const { success, createdSignalsCount, @@ -840,8 +828,7 @@ describe('searchAfterAndBulkCreate', () => { errors, } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - gap: null, - previousStartedAt: new Date(), + tuples, listClient, exceptionsList: [], services: mockService, @@ -873,7 +860,6 @@ describe('searchAfterAndBulkCreate', () => { }); it('invokes the enrichment callback with signal search results', async () => { - const sampleParams = sampleRuleAlertParams(30); mockService.callCluster .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) .mockResolvedValueOnce({ @@ -917,8 +903,7 @@ describe('searchAfterAndBulkCreate', () => { const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ enrichment: mockEnrichment, ruleParams: sampleParams, - gap: moment.duration(2, 'm'), - previousStartedAt: moment().subtract(10, 'm').toDate(), + tuples, listClient, exceptionsList: [], services: mockService, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index 061aa4bba5a41..1dd3a2d2173a8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -17,7 +17,6 @@ import { createSearchResultReturnType, createSearchAfterReturnTypeFromResponse, createTotalHitsFromSearchResult, - getSignalTimeTuples, mergeReturns, mergeSearchResults, } from './utils'; @@ -25,8 +24,7 @@ import { SearchAfterAndBulkCreateParams, SearchAfterAndBulkCreateReturnType } fr // search_after through documents and re-index using bulk endpoint. export const searchAfterAndBulkCreate = async ({ - gap, - previousStartedAt, + tuples: totalToFromTuples, ruleParams, exceptionsList, services, @@ -64,16 +62,6 @@ export const searchAfterAndBulkCreate = async ({ // to ensure we don't exceed maxSignals let signalsCreatedCount = 0; - const totalToFromTuples = getSignalTimeTuples({ - logger, - ruleParamsFrom: ruleParams.from, - ruleParamsTo: ruleParams.to, - ruleParamsMaxSignals: ruleParams.maxSignals, - gap, - previousStartedAt, - interval, - buildRuleMessage, - }); const tuplesToBeLogged = [...totalToFromTuples]; logger.debug(buildRuleMessage(`totalToFromTuples: ${totalToFromTuples.length}`)); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index a79961eb716fd..cadc6d0c5b7c0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -11,14 +11,7 @@ import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; import { signalRulesAlertType } from './signal_rule_alert_type'; import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; import { ruleStatusServiceFactory } from './rule_status_service'; -import { - getGapBetweenRuns, - getGapMaxCatchupRatio, - getListsClient, - getExceptions, - sortExceptionItems, - checkPrivileges, -} from './utils'; +import { getListsClient, getExceptions, sortExceptionItems, checkPrivileges } from './utils'; import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { RuleExecutorOptions, SearchAfterAndBulkCreateReturnType } from './types'; import { searchAfterAndBulkCreate } from './search_after_bulk_create'; @@ -40,8 +33,6 @@ jest.mock('./utils', () => { const original = jest.requireActual('./utils'); return { ...original, - getGapBetweenRuns: jest.fn(), - getGapMaxCatchupRatio: jest.fn(), getListsClient: jest.fn(), getExceptions: jest.fn(), sortExceptionItems: jest.fn(), @@ -113,7 +104,6 @@ describe('rules_notification_alert_type', () => { warning: jest.fn(), }; (ruleStatusServiceFactory as jest.Mock).mockReturnValue(ruleStatusService); - (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(0)); (getListsClient as jest.Mock).mockReturnValue({ listClient: getListClientMock(), exceptionsClient: getExceptionListClientMock(), @@ -124,7 +114,6 @@ describe('rules_notification_alert_type', () => { exceptionsWithValueLists: [], }); (searchAfterAndBulkCreate as jest.Mock).mockClear(); - (getGapMaxCatchupRatio as jest.Mock).mockClear(); (searchAfterAndBulkCreate as jest.Mock).mockResolvedValue({ success: true, searchAfterTimes: [], @@ -187,23 +176,12 @@ describe('rules_notification_alert_type', () => { describe('executor', () => { it('should warn about the gap between runs if gap is very large', async () => { - (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(100, 'm')); - (getGapMaxCatchupRatio as jest.Mock).mockReturnValue({ - maxCatchup: 4, - ratio: 20, - gapDiffInUnits: 95, - }); + payload.previousStartedAt = moment().subtract(100, 'm').toDate(); await alert.executor(payload); expect(logger.warn).toHaveBeenCalled(); - expect(logger.warn.mock.calls[0][0]).toContain( - '2 hours (6000000ms) has passed since last rule execution, and signals may have been missed.' - ); expect(ruleStatusService.error).toHaveBeenCalled(); - expect(ruleStatusService.error.mock.calls[0][0]).toContain( - '2 hours (6000000ms) has passed since last rule execution, and signals may have been missed.' - ); expect(ruleStatusService.error.mock.calls[0][1]).toEqual({ - gap: '2 hours', + gap: 'an hour', }); }); @@ -257,12 +235,7 @@ describe('rules_notification_alert_type', () => { }); it('should NOT warn about the gap between runs if gap small', async () => { - (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(1, 'm')); - (getGapMaxCatchupRatio as jest.Mock).mockReturnValue({ - maxCatchup: 1, - ratio: 1, - gapDiffInUnits: 1, - }); + payload.previousStartedAt = moment().subtract(10, 'm').toDate(); await alert.executor(payload); expect(logger.warn).toHaveBeenCalledTimes(0); expect(ruleStatusService.error).toHaveBeenCalledTimes(0); @@ -450,6 +423,7 @@ describe('rules_notification_alert_type', () => { const ruleAlert = getMlResult(); ruleAlert.params.anomalyThreshold = undefined; payload = getPayload(ruleAlert, alertServices) as jest.Mocked; + payload.previousStartedAt = null; await alert.executor(payload); expect(logger.error).toHaveBeenCalled(); expect(logger.error.mock.calls[0][0]).toContain( @@ -460,6 +434,7 @@ describe('rules_notification_alert_type', () => { it('should throw an error if Machine learning job summary was null', async () => { const ruleAlert = getMlResult(); payload = getPayload(ruleAlert, alertServices) as jest.Mocked; + payload.previousStartedAt = null; jobsSummaryMock.mockResolvedValue([]); await alert.executor(payload); expect(logger.warn).toHaveBeenCalled(); @@ -473,6 +448,7 @@ describe('rules_notification_alert_type', () => { it('should log an error if Machine learning job was not started', async () => { const ruleAlert = getMlResult(); payload = getPayload(ruleAlert, alertServices) as jest.Mocked; + payload.previousStartedAt = null; jobsSummaryMock.mockResolvedValue([ { id: 'some_job_id', @@ -518,6 +494,7 @@ describe('rules_notification_alert_type', () => { it('should call ruleStatusService.success if signals were created', async () => { const ruleAlert = getMlResult(); payload = getPayload(ruleAlert, alertServices) as jest.Mocked; + payload.previousStartedAt = null; jobsSummaryMock.mockResolvedValue([ { id: 'some_job_id', @@ -544,6 +521,7 @@ describe('rules_notification_alert_type', () => { it('should not call checkPrivileges if ML rule', async () => { const ruleAlert = getMlResult(); payload = getPayload(ruleAlert, alertServices) as jest.Mocked; + payload.previousStartedAt = null; jobsSummaryMock.mockResolvedValue([ { id: 'some_job_id', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 98c9dd41d179c..2025ba512cb65 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -37,11 +37,8 @@ import { WrappedSignalHit, } from './types'; import { - getGapBetweenRuns, getListsClient, getExceptions, - getGapMaxCatchupRatio, - MAX_RULE_GAP_RATIO, wrapSignal, createErrorsFromShard, createSearchAfterReturnType, @@ -50,6 +47,7 @@ import { checkPrivileges, hasTimestampFields, hasReadIndexPrivileges, + getRuleRangeTuples, } from './utils'; import { signalParamsSchema } from './signal_params_schema'; import { siemRuleActionGroups } from './siem_rule_action_groups'; @@ -230,29 +228,24 @@ export const signalRulesAlertType = ({ } catch (exc) { logger.error(buildRuleMessage(`Check privileges failed to execute ${exc}`)); } - - const gap = getGapBetweenRuns({ previousStartedAt, interval, from, to }); - if (gap != null && gap.asMilliseconds() > 0) { - const fromUnit = from[from.length - 1]; - const { ratio } = getGapMaxCatchupRatio({ - logger, - buildRuleMessage, - previousStartedAt, - ruleParamsFrom: from, - interval, - unit: fromUnit, - }); - if (ratio && ratio >= MAX_RULE_GAP_RATIO) { - const gapString = gap.humanize(); - const gapMessage = buildRuleMessage( - `${gapString} (${gap.asMilliseconds()}ms) has passed since last rule execution, and signals may have been missed.`, - 'Consider increasing your look behind time or adding more Kibana instances.' - ); - logger.warn(gapMessage); - - hasError = true; - await ruleStatusService.error(gapMessage, { gap: gapString }); - } + const { tuples, remainingGap } = getRuleRangeTuples({ + logger, + previousStartedAt, + from, + to, + interval, + maxSignals, + buildRuleMessage, + }); + if (remainingGap.asMilliseconds() > 0) { + const gapString = remainingGap.humanize(); + const gapMessage = buildRuleMessage( + `${gapString} (${remainingGap.asMilliseconds()}ms) were not queried between this rule execution and the last execution, so signals may have been missed.`, + 'Consider increasing your look behind time or adding more Kibana instances.' + ); + logger.warn(gapMessage); + hasError = true; + await ruleStatusService.error(gapMessage, { gap: gapString }); } try { const { listClient, exceptionsClient } = getListsClient({ @@ -479,6 +472,7 @@ export const signalRulesAlertType = ({ } const inputIndex = await getInputIndex(services, version, index); result = await createThreatSignals({ + tuples, threatMapping, query, inputIndex, @@ -489,8 +483,6 @@ export const signalRulesAlertType = ({ savedId, services, exceptionItems: exceptionItems ?? [], - gap, - previousStartedAt, listClient, logger, eventsTelemetry, @@ -531,8 +523,7 @@ export const signalRulesAlertType = ({ }); result = await searchAfterAndBulkCreate({ - gap, - previousStartedAt, + tuples, listClient, exceptionsList: exceptionItems ?? [], ruleParams: params, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts index ba428bc077125..d9c72f7f95679 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts @@ -13,6 +13,7 @@ import { CreateThreatSignalOptions } from './types'; import { SearchAfterAndBulkCreateReturnType } from '../types'; export const createThreatSignal = async ({ + tuples, threatMapping, threatEnrichment, query, @@ -23,8 +24,6 @@ export const createThreatSignal = async ({ savedId, services, exceptionItems, - gap, - previousStartedAt, listClient, logger, eventsTelemetry, @@ -80,8 +79,7 @@ export const createThreatSignal = async ({ ); const result = await searchAfterAndBulkCreate({ - gap, - previousStartedAt, + tuples, listClient, exceptionsList: exceptionItems, ruleParams: params, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts index e45aea29c423f..854c2b8f3fdc1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts @@ -15,6 +15,7 @@ import { combineConcurrentResults } from './utils'; import { buildThreatEnrichment } from './build_threat_enrichment'; export const createThreatSignals = async ({ + tuples, threatMapping, query, inputIndex, @@ -24,8 +25,6 @@ export const createThreatSignals = async ({ savedId, services, exceptionItems, - gap, - previousStartedAt, listClient, logger, eventsTelemetry, @@ -111,6 +110,7 @@ export const createThreatSignals = async ({ const concurrentSearchesPerformed = chunks.map>( (slicedChunk) => createThreatSignal({ + tuples, threatEnrichment, threatMapping, query, @@ -121,8 +121,6 @@ export const createThreatSignals = async ({ savedId, services, exceptionItems, - gap, - previousStartedAt, listClient, logger, eventsTelemetry, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts index a022cbbdd4042..1c35a5af09b38 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts @@ -6,7 +6,6 @@ */ import { SearchResponse } from 'elasticsearch'; -import { Duration } from 'moment'; import { ListClient } from '../../../../../../lists/server'; import { @@ -34,11 +33,12 @@ import { ILegacyScopedClusterClient, Logger } from '../../../../../../../../src/ import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { TelemetryEventsSender } from '../../../telemetry/sender'; import { BuildRuleMessage } from '../rule_messages'; -import { SearchAfterAndBulkCreateReturnType, SignalsEnrichment } from '../types'; +import { RuleRangeTuple, SearchAfterAndBulkCreateReturnType, SignalsEnrichment } from '../types'; export type SortOrderOrUndefined = 'asc' | 'desc' | undefined; export interface CreateThreatSignalsOptions { + tuples: RuleRangeTuple[]; threatMapping: ThreatMapping; query: string; inputIndex: string[]; @@ -48,8 +48,6 @@ export interface CreateThreatSignalsOptions { savedId: string | undefined; services: AlertServices; exceptionItems: ExceptionListItemSchema[]; - gap: Duration | null; - previousStartedAt: Date | null; listClient: ListClient; logger: Logger; eventsTelemetry: TelemetryEventsSender | undefined; @@ -79,6 +77,7 @@ export interface CreateThreatSignalsOptions { } export interface CreateThreatSignalOptions { + tuples: RuleRangeTuple[]; threatMapping: ThreatMapping; threatEnrichment: SignalsEnrichment; query: string; @@ -89,8 +88,6 @@ export interface CreateThreatSignalOptions { savedId: string | undefined; services: AlertServices; exceptionItems: ExceptionListItemSchema[]; - gap: Duration | null; - previousStartedAt: Date | null; listClient: ListClient; logger: Logger; eventsTelemetry: TelemetryEventsSender | undefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index e5ca1f6a60456..f759da31566e2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -73,6 +73,12 @@ export interface ThresholdSignalHistory { [hash: string]: ThresholdSignalHistoryRecord; } +export interface RuleRangeTuple { + to: moment.Moment; + from: moment.Moment; + maxSignals: number; +} + export interface SignalSource { [key: string]: SearchTypes; // TODO: SignalSource is being used as the type for documents matching detection engine queries, but they may not @@ -251,8 +257,11 @@ export interface QueryFilter { export type SignalsEnrichment = (signals: SignalSearchResponse) => Promise; export interface SearchAfterAndBulkCreateParams { - gap: moment.Duration | null; - previousStartedAt: Date | null | undefined; + tuples: Array<{ + to: moment.Moment; + from: moment.Moment; + maxSignals: number; + }>; ruleParams: RuleTypeParams; services: AlertServices; listClient: ListClient; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 7888bb6deaab7..0a581816ee82f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -25,10 +25,10 @@ import { parseInterval, getDriftTolerance, getGapBetweenRuns, - getGapMaxCatchupRatio, + getNumCatchupIntervals, errorAggregator, getListsClient, - getSignalTimeTuples, + getRuleRangeTuples, getExceptions, hasTimestampFields, wrapBuildingBlocks, @@ -109,7 +109,7 @@ describe('utils', () => { expect(duration?.asMilliseconds()).toEqual(moment.duration(5, 'minutes').asMilliseconds()); }); - test('it returns null given an invalid duration', () => { + test('it throws given an invalid duration', () => { const duration = parseInterval('junk'); expect(duration).toBeNull(); }); @@ -148,7 +148,7 @@ describe('utils', () => { const drift = getDriftTolerance({ from: 'now-6m', to: 'now', - interval: moment.duration(5, 'minutes'), + intervalDuration: moment.duration(5, 'minutes'), }); expect(drift).not.toBeNull(); expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); @@ -158,7 +158,7 @@ describe('utils', () => { const drift = getDriftTolerance({ from: 'now-5m', to: 'now', - interval: moment.duration(5, 'minutes'), + intervalDuration: moment.duration(5, 'minutes'), }); expect(drift?.asMilliseconds()).toEqual(0); }); @@ -167,7 +167,7 @@ describe('utils', () => { const drift = getDriftTolerance({ from: 'now-10m', to: 'now', - interval: moment.duration(5, 'minutes'), + intervalDuration: moment.duration(5, 'minutes'), }); expect(drift).not.toBeNull(); expect(drift?.asMilliseconds()).toEqual(moment.duration(5, 'minutes').asMilliseconds()); @@ -177,7 +177,7 @@ describe('utils', () => { const drift = getDriftTolerance({ from: 'now-10m', to: 'now', - interval: moment.duration(0, 'milliseconds'), + intervalDuration: moment.duration(0, 'milliseconds'), }); expect(drift).not.toBeNull(); expect(drift?.asMilliseconds()).toEqual(moment.duration(10, 'minutes').asMilliseconds()); @@ -187,7 +187,7 @@ describe('utils', () => { const drift = getDriftTolerance({ from: 'invalid', to: 'now', - interval: moment.duration(5, 'minutes'), + intervalDuration: moment.duration(5, 'minutes'), }); expect(drift).not.toBeNull(); expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); @@ -197,7 +197,7 @@ describe('utils', () => { const drift = getDriftTolerance({ from: '10m', to: 'now', - interval: moment.duration(5, 'minutes'), + intervalDuration: moment.duration(5, 'minutes'), }); expect(drift).not.toBeNull(); expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); @@ -207,7 +207,7 @@ describe('utils', () => { const drift = getDriftTolerance({ from: 'now-10m', to: 'now-1m', - interval: moment.duration(5, 'minutes'), + intervalDuration: moment.duration(5, 'minutes'), }); expect(drift).not.toBeNull(); expect(drift?.asMilliseconds()).toEqual(moment.duration(4, 'minutes').asMilliseconds()); @@ -217,7 +217,7 @@ describe('utils', () => { const drift = getDriftTolerance({ from: moment().subtract(10, 'minutes').toISOString(), to: 'now', - interval: moment.duration(5, 'minutes'), + intervalDuration: moment.duration(5, 'minutes'), }); expect(drift).not.toBeNull(); expect(drift?.asMilliseconds()).toEqual(moment.duration(5, 'minutes').asMilliseconds()); @@ -227,7 +227,7 @@ describe('utils', () => { const drift = getDriftTolerance({ from: 'now-6m', to: moment().toISOString(), - interval: moment.duration(5, 'minutes'), + intervalDuration: moment.duration(5, 'minutes'), }); expect(drift).not.toBeNull(); expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); @@ -238,7 +238,7 @@ describe('utils', () => { test('it returns a gap of 0 when "from" and interval match each other and the previous started was from the previous interval time', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(5, 'minutes').toDate(), - interval: '5m', + intervalDuration: moment.duration(5, 'minutes'), from: 'now-5m', to: 'now', now: nowDate.clone(), @@ -250,7 +250,7 @@ describe('utils', () => { test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(5, 'minutes').toDate(), - interval: '5m', + intervalDuration: moment.duration(5, 'minutes'), from: 'now-6m', to: 'now', now: nowDate.clone(), @@ -262,7 +262,7 @@ describe('utils', () => { test('it returns a negative gap of 5 minutes when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(5, 'minutes').toDate(), - interval: '5m', + intervalDuration: moment.duration(5, 'minutes'), from: 'now-10m', to: 'now', now: nowDate.clone(), @@ -274,7 +274,7 @@ describe('utils', () => { test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 10 minutes ago and so was the interval', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(10, 'minutes').toDate(), - interval: '10m', + intervalDuration: moment.duration(10, 'minutes'), from: 'now-11m', to: 'now', now: nowDate.clone(), @@ -286,7 +286,7 @@ describe('utils', () => { test('it returns a gap of only -30 seconds when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is 30 seconds more', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(5, 'minutes').subtract(30, 'seconds').toDate(), - interval: '5m', + intervalDuration: moment.duration(5, 'minutes'), from: 'now-6m', to: 'now', now: nowDate.clone(), @@ -298,7 +298,7 @@ describe('utils', () => { test('it returns an exact 0 gap when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is one minute late', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(6, 'minutes').toDate(), - interval: '5m', + intervalDuration: moment.duration(5, 'minutes'), from: 'now-6m', to: 'now', now: nowDate.clone(), @@ -310,7 +310,7 @@ describe('utils', () => { test('it returns a gap of 30 seconds when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is one minute and 30 seconds late', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(6, 'minutes').subtract(30, 'seconds').toDate(), - interval: '5m', + intervalDuration: moment.duration(5, 'minutes'), from: 'now-6m', to: 'now', now: nowDate.clone(), @@ -322,7 +322,7 @@ describe('utils', () => { test('it returns a gap of 1 minute when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is two minutes late', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(7, 'minutes').toDate(), - interval: '5m', + intervalDuration: moment.duration(5, 'minutes'), from: 'now-6m', to: 'now', now: nowDate.clone(), @@ -331,32 +331,21 @@ describe('utils', () => { expect(gap?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); }); - test('it returns null if given a previousStartedAt of null', () => { + test('it returns 0 if given a previousStartedAt of null', () => { const gap = getGapBetweenRuns({ previousStartedAt: null, - interval: '5m', + intervalDuration: moment.duration(5, 'minutes'), from: 'now-5m', to: 'now', now: nowDate.clone(), }); - expect(gap).toBeNull(); - }); - - test('it returns null if the interval is an invalid string such as "invalid"', () => { - const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone().toDate(), - interval: 'invalid', // if not set to "x" where x is an interval such as 6m - from: 'now-5m', - to: 'now', - now: nowDate.clone(), - }); - expect(gap).toBeNull(); + expect(gap.asMilliseconds()).toEqual(0); }); test('it returns the expected result when "from" is an invalid string such as "invalid"', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(7, 'minutes').toDate(), - interval: '5m', + intervalDuration: moment.duration(5, 'minutes'), from: 'invalid', to: 'now', now: nowDate.clone(), @@ -368,7 +357,7 @@ describe('utils', () => { test('it returns the expected result when "to" is an invalid string such as "invalid"', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(7, 'minutes').toDate(), - interval: '5m', + intervalDuration: moment.duration(5, 'minutes'), from: 'now-6m', to: 'invalid', now: nowDate.clone(), @@ -609,134 +598,116 @@ describe('utils', () => { }); }); - describe('getSignalTimeTuples', () => { + describe('getRuleRangeTuples', () => { test('should return a single tuple if no gap', () => { - const someTuples = getSignalTimeTuples({ + const { tuples, remainingGap } = getRuleRangeTuples({ logger: mockLogger, - gap: null, previousStartedAt: moment().subtract(30, 's').toDate(), interval: '30s', - ruleParamsFrom: 'now-30s', - ruleParamsTo: 'now', - ruleParamsMaxSignals: 20, + from: 'now-30s', + to: 'now', + maxSignals: 20, buildRuleMessage, }); - const someTuple = someTuples[0]; + const someTuple = tuples[0]; expect(moment(someTuple.to).diff(moment(someTuple.from), 's')).toEqual(30); + expect(tuples.length).toEqual(1); + expect(remainingGap.asMilliseconds()).toEqual(0); + }); + + test('should return a single tuple if malformed interval prevents gap calculation', () => { + const { tuples, remainingGap } = getRuleRangeTuples({ + logger: mockLogger, + previousStartedAt: moment().subtract(30, 's').toDate(), + interval: 'invalid', + from: 'now-30s', + to: 'now', + maxSignals: 20, + buildRuleMessage, + }); + const someTuple = tuples[0]; + expect(moment(someTuple.to).diff(moment(someTuple.from), 's')).toEqual(30); + expect(tuples.length).toEqual(1); + expect(remainingGap.asMilliseconds()).toEqual(0); }); test('should return two tuples if gap and previouslyStartedAt', () => { - const someTuples = getSignalTimeTuples({ + const { tuples, remainingGap } = getRuleRangeTuples({ logger: mockLogger, - gap: moment.duration(10, 's'), previousStartedAt: moment().subtract(65, 's').toDate(), interval: '50s', - ruleParamsFrom: 'now-55s', - ruleParamsTo: 'now', - ruleParamsMaxSignals: 20, + from: 'now-55s', + to: 'now', + maxSignals: 20, buildRuleMessage, }); - const someTuple = someTuples[1]; - expect(moment(someTuple.to).diff(moment(someTuple.from), 's')).toEqual(10); + const someTuple = tuples[1]; + expect(moment(someTuple.to).diff(moment(someTuple.from), 's')).toEqual(55); + expect(remainingGap.asMilliseconds()).toEqual(0); }); test('should return five tuples when give long gap', () => { - const someTuples = getSignalTimeTuples({ + const { tuples, remainingGap } = getRuleRangeTuples({ logger: mockLogger, - gap: moment.duration(65, 's'), // 64 is 5 times the interval + lookback, which will trigger max lookback - previousStartedAt: moment().subtract(65, 's').toDate(), + previousStartedAt: moment().subtract(65, 's').toDate(), // 64 is 5 times the interval + lookback, which will trigger max lookback interval: '10s', - ruleParamsFrom: 'now-13s', - ruleParamsTo: 'now', - ruleParamsMaxSignals: 20, + from: 'now-13s', + to: 'now', + maxSignals: 20, buildRuleMessage, }); - expect(someTuples.length).toEqual(5); - someTuples.forEach((item, index) => { + expect(tuples.length).toEqual(5); + tuples.forEach((item, index) => { if (index === 0) { return; } - expect(moment(item.to).diff(moment(item.from), 's')).toEqual(10); + expect(moment(item.to).diff(moment(item.from), 's')).toEqual(13); + expect(item.to.diff(tuples[index - 1].to, 's')).toEqual(-10); + expect(item.from.diff(tuples[index - 1].from, 's')).toEqual(-10); }); + expect(remainingGap.asMilliseconds()).toEqual(12000); }); - // this tests if calculatedFrom in utils.ts:320 parses an int and not a float - // if we don't parse as an int, then dateMath.parse will fail - // as it doesn't support parsing `now-67.549`, it only supports ints like `now-67`. - test('should return five tuples when given a gap with a decimal to ensure no parsing errors', () => { - const someTuples = getSignalTimeTuples({ + test('should return a single tuple when give a negative gap (rule ran sooner than expected)', () => { + const { tuples, remainingGap } = getRuleRangeTuples({ logger: mockLogger, - gap: moment.duration(67549, 'ms'), // 64 is 5 times the interval + lookback, which will trigger max lookback - previousStartedAt: moment().subtract(67549, 'ms').toDate(), - interval: '10s', - ruleParamsFrom: 'now-13s', - ruleParamsTo: 'now', - ruleParamsMaxSignals: 20, - buildRuleMessage, - }); - expect(someTuples.length).toEqual(5); - }); - - test('should return single tuples when give a negative gap (rule ran sooner than expected)', () => { - const someTuples = getSignalTimeTuples({ - logger: mockLogger, - gap: moment.duration(-15, 's'), // 64 is 5 times the interval + lookback, which will trigger max lookback previousStartedAt: moment().subtract(-15, 's').toDate(), interval: '10s', - ruleParamsFrom: 'now-13s', - ruleParamsTo: 'now', - ruleParamsMaxSignals: 20, + from: 'now-13s', + to: 'now', + maxSignals: 20, buildRuleMessage, }); - expect(someTuples.length).toEqual(1); - const someTuple = someTuples[0]; + expect(tuples.length).toEqual(1); + const someTuple = tuples[0]; expect(moment(someTuple.to).diff(moment(someTuple.from), 's')).toEqual(13); + expect(remainingGap.asMilliseconds()).toEqual(0); }); }); describe('getMaxCatchupRatio', () => { - test('should return null if rule has never run before', () => { - const { maxCatchup, ratio, gapDiffInUnits } = getGapMaxCatchupRatio({ - logger: mockLogger, - previousStartedAt: null, - interval: '30s', - ruleParamsFrom: 'now-30s', - buildRuleMessage, - unit: 's', + test('should return 0 if gap is 0', () => { + const catchup = getNumCatchupIntervals({ + gap: moment.duration(0), + intervalDuration: moment.duration(11000), }); - expect(maxCatchup).toBeNull(); - expect(ratio).toBeNull(); - expect(gapDiffInUnits).toBeNull(); + expect(catchup).toEqual(0); }); - test('should should have non-null values when gap is present', () => { - const { maxCatchup, ratio, gapDiffInUnits } = getGapMaxCatchupRatio({ - logger: mockLogger, - previousStartedAt: moment().subtract(65, 's').toDate(), - interval: '50s', - ruleParamsFrom: 'now-55s', - buildRuleMessage, - unit: 's', + test('should return 1 if gap is in (0, intervalDuration]', () => { + const catchup = getNumCatchupIntervals({ + gap: moment.duration(10000), + intervalDuration: moment.duration(10000), }); - expect(maxCatchup).toEqual(0.2); - expect(ratio).toEqual(0.2); - expect(gapDiffInUnits).toEqual(10); + expect(catchup).toEqual(1); }); - // when a rule runs sooner than expected we don't - // consider that a gap as that is a very rare circumstance - test('should return null when given a negative gap (rule ran sooner than expected)', () => { - const { maxCatchup, ratio, gapDiffInUnits } = getGapMaxCatchupRatio({ - logger: mockLogger, - previousStartedAt: moment().subtract(-15, 's').toDate(), - interval: '10s', - ruleParamsFrom: 'now-13s', - buildRuleMessage, - unit: 's', + test('should round up return value', () => { + const catchup = getNumCatchupIntervals({ + gap: moment.duration(15000), + intervalDuration: moment.duration(11000), }); - expect(maxCatchup).toBeNull(); - expect(ratio).toBeNull(); - expect(gapDiffInUnits).toBeNull(); + expect(catchup).toEqual(2); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 58bf22be97bf8..2b306cd2a8d9d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -29,12 +29,12 @@ import { ListArray } from '../../../../common/detection_engine/schemas/types/lis import { BulkResponse, BulkResponseErrorAggregation, - isValidUnit, SignalHit, SearchAfterAndBulkCreateReturnType, SignalSearchResponse, Signal, WrappedSignalHit, + RuleRangeTuple, } from './types'; import { BuildRuleMessage } from './rule_messages'; import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; @@ -163,82 +163,21 @@ export const checkPrivileges = async ( }, }); -export const getGapMaxCatchupRatio = ({ - logger, - previousStartedAt, - unit, - buildRuleMessage, - ruleParamsFrom, - interval, +export const getNumCatchupIntervals = ({ + gap, + intervalDuration, }: { - logger: Logger; - ruleParamsFrom: string; - previousStartedAt: Date | null | undefined; - interval: string; - buildRuleMessage: BuildRuleMessage; - unit: string; -}): { - maxCatchup: number | null; - ratio: number | null; - gapDiffInUnits: number | null; -} => { - if (previousStartedAt == null) { - return { - maxCatchup: null, - ratio: null, - gapDiffInUnits: null, - }; - } - if (!isValidUnit(unit)) { - logger.error(buildRuleMessage(`unit: ${unit} failed isValidUnit check`)); - return { - maxCatchup: null, - ratio: null, - gapDiffInUnits: null, - }; - } - /* - we need the total duration from now until the last time the rule ran. - the next few lines can be summed up as calculating - "how many second | minutes | hours have passed since the last time this ran?" - */ - const nowToGapDiff = moment.duration(moment().diff(previousStartedAt)); - // rule ran early, no gap - if (shorthandMap[unit].asFn(nowToGapDiff) < 0) { - // rule ran early, no gap - return { - maxCatchup: null, - ratio: null, - gapDiffInUnits: null, - }; - } - const calculatedFrom = `now-${ - parseInt(shorthandMap[unit].asFn(nowToGapDiff).toString(), 10) + unit - }`; - logger.debug(buildRuleMessage(`calculatedFrom: ${calculatedFrom}`)); - - const intervalMoment = moment.duration(parseInt(interval, 10), unit); - logger.debug(buildRuleMessage(`intervalMoment: ${shorthandMap[unit].asFn(intervalMoment)}`)); - const calculatedFromAsMoment = dateMath.parse(calculatedFrom); - const dateMathRuleParamsFrom = dateMath.parse(ruleParamsFrom); - if (dateMathRuleParamsFrom != null && intervalMoment != null) { - const momentUnit = shorthandMap[unit].momentString as moment.DurationInputArg2; - const gapDiffInUnits = dateMathRuleParamsFrom.diff(calculatedFromAsMoment, momentUnit); - - const ratio = gapDiffInUnits / shorthandMap[unit].asFn(intervalMoment); - - // maxCatchup is to ensure we are not trying to catch up too far back. - // This allows for a maximum of 4 consecutive rule execution misses - // to be included in the number of signals generated. - const maxCatchup = ratio < MAX_RULE_GAP_RATIO ? ratio : MAX_RULE_GAP_RATIO; - return { maxCatchup, ratio, gapDiffInUnits }; + gap: moment.Duration; + intervalDuration: moment.Duration; +}): number => { + if (gap.asMilliseconds() <= 0 || intervalDuration.asMilliseconds() <= 0) { + return 0; } - logger.error(buildRuleMessage('failed to parse calculatedFrom and intervalMoment')); - return { - maxCatchup: null, - ratio: null, - gapDiffInUnits: null, - }; + const ratio = Math.ceil(gap.asMilliseconds() / intervalDuration.asMilliseconds()); + // maxCatchup is to ensure we are not trying to catch up too far back. + // This allows for a maximum of 4 consecutive rule execution misses + // to be included in the number of signals generated. + return ratio < MAX_RULE_GAP_RATIO ? ratio : MAX_RULE_GAP_RATIO; }; export const getListsClient = ({ @@ -396,50 +335,40 @@ export const parseInterval = (intervalString: string): moment.Duration | null => export const getDriftTolerance = ({ from, to, - interval, + intervalDuration, now = moment(), }: { from: string; to: string; - interval: moment.Duration; + intervalDuration: moment.Duration; now?: moment.Moment; -}): moment.Duration | null => { +}): moment.Duration => { const toDate = parseScheduleDates(to) ?? now; const fromDate = parseScheduleDates(from) ?? dateMath.parse('now-6m'); const timeSegment = toDate.diff(fromDate); const duration = moment.duration(timeSegment); - if (duration !== null) { - return duration.subtract(interval); - } else { - return null; - } + return duration.subtract(intervalDuration); }; export const getGapBetweenRuns = ({ previousStartedAt, - interval, + intervalDuration, from, to, now = moment(), }: { previousStartedAt: Date | undefined | null; - interval: string; + intervalDuration: moment.Duration; from: string; to: string; now?: moment.Moment; -}): moment.Duration | null => { +}): moment.Duration => { if (previousStartedAt == null) { - return null; - } - const intervalDuration = parseInterval(interval); - if (intervalDuration == null) { - return null; - } - const driftTolerance = getDriftTolerance({ from, to, interval: intervalDuration }); - if (driftTolerance == null) { - return null; + return moment.duration(0); } + const driftTolerance = getDriftTolerance({ from, to, intervalDuration }); + const diff = moment.duration(now.diff(previousStartedAt)); const drift = diff.subtract(intervalDuration); return drift.subtract(driftTolerance); @@ -489,135 +418,103 @@ export const errorAggregator = ( }, Object.create(null)); }; -/** - * Determines the number of time intervals to search if gap is present - * along with new maxSignals per time interval. - * @param logger Logger - * @param ruleParamsFrom string representing the rules 'from' property - * @param ruleParamsTo string representing the rules 'to' property - * @param ruleParamsMaxSignals int representing the maxSignals property on the rule (usually unmodified at 100) - * @param gap moment.Duration representing a gap in since the last time the rule ran - * @param previousStartedAt Date at which the rule last ran - * @param interval string the interval which the rule runs - * @param buildRuleMessage function provides meta information for logged event - */ -export const getSignalTimeTuples = ({ +export const getRuleRangeTuples = ({ logger, - ruleParamsFrom, - ruleParamsTo, - ruleParamsMaxSignals, - gap, previousStartedAt, + from, + to, interval, + maxSignals, buildRuleMessage, }: { logger: Logger; - ruleParamsFrom: string; - ruleParamsTo: string; - ruleParamsMaxSignals: number; - gap: moment.Duration | null; previousStartedAt: Date | null | undefined; + from: string; + to: string; interval: string; - buildRuleMessage: BuildRuleMessage; -}): Array<{ - to: moment.Moment | undefined; - from: moment.Moment | undefined; maxSignals: number; -}> => { - let totalToFromTuples: Array<{ - to: moment.Moment | undefined; - from: moment.Moment | undefined; - maxSignals: number; - }> = []; - if (gap != null && gap.valueOf() > 0 && previousStartedAt != null) { - const fromUnit = ruleParamsFrom[ruleParamsFrom.length - 1]; - if (isValidUnit(fromUnit)) { - const unit = fromUnit; // only seconds (s), minutes (m) or hours (h) - - /* - we need the total duration from now until the last time the rule ran. - the next few lines can be summed up as calculating - "how many second | minutes | hours have passed since the last time this ran?" - */ - const nowToGapDiff = moment.duration(moment().diff(previousStartedAt)); - const calculatedFrom = `now-${ - parseInt(shorthandMap[unit].asFn(nowToGapDiff).toString(), 10) + unit - }`; - logger.debug(buildRuleMessage(`calculatedFrom: ${calculatedFrom}`)); - - const intervalMoment = moment.duration(parseInt(interval, 10), unit); - logger.debug(buildRuleMessage(`intervalMoment: ${shorthandMap[unit].asFn(intervalMoment)}`)); - const momentUnit = shorthandMap[unit].momentString as moment.DurationInputArg2; - // maxCatchup is to ensure we are not trying to catch up too far back. - // This allows for a maximum of 4 consecutive rule execution misses - // to be included in the number of signals generated. - const { maxCatchup, ratio, gapDiffInUnits } = getGapMaxCatchupRatio({ - logger, - buildRuleMessage, - previousStartedAt, - unit, - ruleParamsFrom, - interval, - }); - logger.debug(buildRuleMessage(`maxCatchup: ${maxCatchup}, ratio: ${ratio}`)); - if (maxCatchup == null || ratio == null || gapDiffInUnits == null) { - throw new Error( - buildRuleMessage('failed to calculate maxCatchup, ratio, or gapDiffInUnits') - ); - } - let tempTo = dateMath.parse(ruleParamsFrom); - if (tempTo == null) { - // return an error - throw new Error(buildRuleMessage('dateMath parse failed')); - } - - let beforeMutatedFrom: moment.Moment | undefined; - while (totalToFromTuples.length < maxCatchup) { - // if maxCatchup is less than 1, we calculate the 'from' differently - // and maxSignals becomes some less amount of maxSignals - // in order to maintain maxSignals per full rule interval. - if (maxCatchup > 0 && maxCatchup < 1) { - totalToFromTuples.push({ - to: tempTo.clone(), - from: tempTo.clone().subtract(gapDiffInUnits, momentUnit), - maxSignals: ruleParamsMaxSignals * maxCatchup, - }); - break; - } - const beforeMutatedTo = tempTo.clone(); - - // moment.subtract mutates the moment so we need to clone again.. - beforeMutatedFrom = tempTo.clone().subtract(intervalMoment, momentUnit); - const tuple = { - to: beforeMutatedTo, - from: beforeMutatedFrom, - maxSignals: ruleParamsMaxSignals, - }; - totalToFromTuples = [...totalToFromTuples, tuple]; - tempTo = beforeMutatedFrom; - } - totalToFromTuples = [ - { - to: dateMath.parse(ruleParamsTo), - from: dateMath.parse(ruleParamsFrom), - maxSignals: ruleParamsMaxSignals, - }, - ...totalToFromTuples, - ]; - } - } else { - totalToFromTuples = [ - { - to: dateMath.parse(ruleParamsTo), - from: dateMath.parse(ruleParamsFrom), - maxSignals: ruleParamsMaxSignals, - }, - ]; + buildRuleMessage: BuildRuleMessage; +}) => { + const originalTo = dateMath.parse(to); + const originalFrom = dateMath.parse(from); + if (originalTo == null || originalFrom == null) { + throw new Error(buildRuleMessage('dateMath parse failed')); + } + const tuples = [ + { + to: originalTo, + from: originalFrom, + maxSignals, + }, + ]; + const intervalDuration = parseInterval(interval); + if (intervalDuration == null) { + logger.error(`Failed to compute gap between rule runs: could not parse rule interval`); + return { tuples, remainingGap: moment.duration(0) }; } - logger.debug( - buildRuleMessage(`totalToFromTuples: ${JSON.stringify(totalToFromTuples, null, 4)}`) + const gap = getGapBetweenRuns({ previousStartedAt, intervalDuration, from, to }); + const catchup = getNumCatchupIntervals({ + gap, + intervalDuration, + }); + const catchupTuples = getCatchupTuples({ + to: originalTo, + from: originalFrom, + ruleParamsMaxSignals: maxSignals, + catchup, + intervalDuration, + }); + tuples.push(...catchupTuples); + // Each extra tuple adds one extra intervalDuration to the time range this rule will cover. + const remainingGapMilliseconds = Math.max( + gap.asMilliseconds() - catchup * intervalDuration.asMilliseconds(), + 0 ); - return totalToFromTuples; + return { tuples, remainingGap: moment.duration(remainingGapMilliseconds) }; +}; + +/** + * Creates rule range tuples needed to cover gaps since the last rule run. + * @param to moment.Moment representing the rules 'to' property + * @param from moment.Moment representing the rules 'from' property + * @param ruleParamsMaxSignals int representing the maxSignals property on the rule (usually unmodified at 100) + * @param catchup number the number of additional rule run intervals to add + * @param intervalDuration moment.Duration the interval which the rule runs + */ +export const getCatchupTuples = ({ + to, + from, + ruleParamsMaxSignals, + catchup, + intervalDuration, +}: { + to: moment.Moment; + from: moment.Moment; + ruleParamsMaxSignals: number; + catchup: number; + intervalDuration: moment.Duration; +}): RuleRangeTuple[] => { + const catchupTuples: RuleRangeTuple[] = []; + const intervalInMilliseconds = intervalDuration.asMilliseconds(); + let currentTo = to; + let currentFrom = from; + // This loop will create tuples with overlapping time ranges, the same way rule runs have overlapping time + // ranges due to the additional lookback. We could choose to create tuples that don't overlap here by using the + // "from" value from one tuple as "to" in the next one, however, the overlap matters for rule types like EQL and + // threshold rules that look for sets of documents within the query. Thus we keep the overlap so that these + // extra tuples behave as similarly to the regular rule runs as possible. + while (catchupTuples.length < catchup) { + const nextTo = currentTo.clone().subtract(intervalInMilliseconds); + const nextFrom = currentFrom.clone().subtract(intervalInMilliseconds); + catchupTuples.push({ + to: nextTo, + from: nextFrom, + maxSignals: ruleParamsMaxSignals, + }); + currentTo = nextTo; + currentFrom = nextFrom; + } + return catchupTuples; }; /**