From 78f744ec856bb1463fd2a9db26572c89930cda32 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 13 May 2021 14:05:02 -0600 Subject: [PATCH] [Security Solutions][Lists] Trims down list plugin size by breaking out the exception builder into chunks by using react lazy loading (#99989) ## Summary Trims down the list plugin size by breaking out the exception builder into a dedicated chunk by using React Suspense and React lazy loading. Before this PR the page load bundle size was `260503`, after the page load bundle size will be `194132`: You can calculate this through: ```ts node ./scripts/build_kibana_platform_plugins --dist --focus lists cat ./x-pack/plugins/lists/target/public/metrics.json ``` Before ```json [ { "group": "@kbn/optimizer bundle module count", "id": "lists", "value": 227 }, { "group": "page load bundle size", "id": "lists", "value": 260503, <--- Very large load bundle size "limit": 280504, "limitConfigPath": "packages/kbn-optimizer/limits.yml" }, { "group": "async chunks size", "id": "lists", "value": 0 }, { "group": "async chunk count", "id": "lists", "value": 0 }, { "group": "miscellaneous assets size", "id": "lists", "value": 0 } ] ``` After: ```json [ { "group": "@kbn/optimizer bundle module count", "id": "lists", "value": 227 }, { "group": "page load bundle size", "id": "lists", "value": 194132, <--- Not as large bundle size "limit": 280504, "limitConfigPath": "packages/kbn-optimizer/limits.yml" }, { "group": "async chunks size", "id": "lists", "value": 70000 }, { "group": "async chunk count", "id": "lists", "value": 1 }, { "group": "miscellaneous assets size", "id": "lists", "value": 0 } ] ``` ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../builder/exception_items_renderer.tsx | 3 ++ .../exceptions/components/builder/index.tsx | 32 ++++++++++++-- .../lists/public/exceptions/transforms.ts | 2 +- .../add_exception_modal/index.test.tsx | 7 +-- .../exceptions/add_exception_modal/index.tsx | 43 +++++++++---------- .../edit_exception_modal/index.test.tsx | 11 ++--- .../exceptions/edit_exception_modal/index.tsx | 41 +++++++++--------- .../view/components/form/index.test.tsx | 6 ++- .../view/components/form/index.tsx | 39 ++++++++--------- 9 files changed, 108 insertions(+), 76 deletions(-) diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx index a698feb93722c..646803f2e6794 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx @@ -444,3 +444,6 @@ export const ExceptionBuilderComponent = ({ }; ExceptionBuilderComponent.displayName = 'ExceptionBuilder'; + +// eslint-disable-next-line import/no-default-export +export default ExceptionBuilderComponent; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx index 833034aa0a542..551889e4a821d 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx @@ -5,6 +5,32 @@ * 2.0. */ -export { BuilderEntryItem } from './entry_renderer'; -export { BuilderExceptionListItemComponent } from './exception_item_renderer'; -export { ExceptionBuilderComponent, OnChangeProps } from './exception_items_renderer'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import React, { Suspense, lazy } from 'react'; + +// Note: Only use import type/export type here to avoid pulling anything non-lazy into the main plugin and increasing the plugin size +import type { ExceptionBuilderProps } from './exception_items_renderer'; +export type { OnChangeProps } from './exception_items_renderer'; + +interface ExtraProps { + dataTestSubj: string; + idAria: string; +} + +/** + * This lazy load allows the exception builder to pull everything out into a plugin chunk. + * You want to be careful of not directly importing/exporting things from exception_items_renderer + * unless you use a import type, and/or a export type to ensure full type erasure + */ +const ExceptionBuilderComponentLazy = lazy(() => import('./exception_items_renderer')); +export const getExceptionBuilderComponentLazy = ( + props: ExceptionBuilderProps & ExtraProps +): JSX.Element => ( + }> + + +); diff --git a/x-pack/plugins/lists/public/exceptions/transforms.ts b/x-pack/plugins/lists/public/exceptions/transforms.ts index 468dfc00ca852..50ce1b6e33a4b 100644 --- a/x-pack/plugins/lists/public/exceptions/transforms.ts +++ b/x-pack/plugins/lists/public/exceptions/transforms.ts @@ -8,7 +8,7 @@ import { flow } from 'fp-ts/lib/function'; import { addIdToItem, removeIdFromItem } from '@kbn/securitysolution-utils'; -import { +import type { CreateExceptionListItemSchema, EntriesArray, Entry, 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 bf15994f60cbc..d659f557ee751 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 @@ -58,13 +58,14 @@ describe('When the add exception modal is opened', () => { ReturnType >; let ExceptionBuilderComponent: jest.SpyInstance< - ReturnType + ReturnType >; beforeEach(() => { + const emptyComp = ; defaultEndpointItems = jest.spyOn(helpers, 'defaultEndpointExceptionItems'); ExceptionBuilderComponent = jest - .spyOn(ExceptionBuilder, 'ExceptionBuilderComponent') - .mockReturnValue(<>); + .spyOn(ExceptionBuilder, 'getExceptionBuilderComponentLazy') + .mockReturnValue(emptyComp); (useAsync as jest.Mock).mockImplementation(() => ({ start: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 96335f8d85d90..120c4ad8efc1b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -469,28 +469,27 @@ export const AddExceptionModal = memo(function AddExceptionModal({ )} - + {ExceptionBuilder.getExceptionBuilderComponentLazy({ + allowLargeValueLists: + !isEqlRule(maybeRule?.type) && !isThresholdRule(maybeRule?.type), + httpService: http, + autocompleteService: data.autocomplete, + exceptionListItems: initialExceptionItems, + listType: exceptionListType, + osTypes: osTypesSelection, + listId: ruleExceptionList.list_id, + listNamespaceType: ruleExceptionList.namespace_type, + listTypeSpecificIndexPatternFilter: filterIndexPatterns, + ruleName, + indexPatterns, + isOrDisabled: isExceptionBuilderFormDisabled, + isAndDisabled: isExceptionBuilderFormDisabled, + isNestedDisabled: isExceptionBuilderFormDisabled, + dataTestSubj: 'alert-exception-builder', + idAria: 'alert-exception-builder', + onChange: handleBuilderOnChange, + isDisabled: isExceptionBuilderFormDisabled, + })} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx index 7ee0e6888a42e..64ef1dead7e75 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx @@ -49,11 +49,11 @@ jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_ jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async'); jest.mock('../../../../shared_imports', () => { const originalModule = jest.requireActual('../../../../shared_imports'); - + const emptyComp = ; return { ...originalModule, ExceptionBuilder: { - ExceptionBuilderComponent: () => ({} as JSX.Element), + getExceptionBuilderComponentLazy: () => emptyComp, }, }; }); @@ -62,13 +62,14 @@ describe('When the edit exception modal is opened', () => { const ruleName = 'test rule'; let ExceptionBuilderComponent: jest.SpyInstance< - ReturnType + ReturnType >; beforeEach(() => { + const emptyComp = ; ExceptionBuilderComponent = jest - .spyOn(ExceptionBuilder, 'ExceptionBuilderComponent') - .mockReturnValue(<>); + .spyOn(ExceptionBuilder, 'getExceptionBuilderComponentLazy') + .mockReturnValue(emptyComp); (useSignalIndex as jest.Mock).mockReturnValue({ loading: false, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 8bf5ea9f8a80f..5fb52994fb0f5 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -342,27 +342,26 @@ export const EditExceptionModal = memo(function EditExceptionModal({ )} - + {ExceptionBuilder.getExceptionBuilderComponentLazy({ + allowLargeValueLists: + !isEqlRule(maybeRule?.type) && !isThresholdRule(maybeRule?.type), + httpService: http, + autocompleteService: data.autocomplete, + exceptionListItems: [exceptionItem], + listType: exceptionListType, + listId: exceptionItem.list_id, + listNamespaceType: exceptionItem.namespace_type, + listTypeSpecificIndexPatternFilter: filterIndexPatterns, + ruleName, + isOrDisabled: true, + isAndDisabled: false, + osTypes: exceptionItem.os_types, + isNestedDisabled: false, + dataTestSubj: 'edit-exception-modal-builder', + idAria: 'edit-exception-modal-builder', + onChange: handleBuilderOnChange, + indexPatterns, + })} diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx index 940882d079a12..0867d0542e4c1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx @@ -17,6 +17,7 @@ import { createGlobalNoMiddlewareStore, ecsEventMock } from '../../../test_utils import { getMockTheme } from '../../../../../../common/lib/kibana/kibana_react.mock'; import { NAME_ERROR, NAME_PLACEHOLDER } from './translations'; import { useCurrentUser, useKibana } from '../../../../../../common/lib/kibana'; +import { ExceptionBuilder } from '../../../../../../shared_imports'; jest.mock('../../../../../../common/lib/kibana'); jest.mock('../../../../../../common/containers/source'); @@ -53,6 +54,9 @@ describe('Event filter form', () => { }; beforeEach(() => { + const emptyComp = ; + jest.spyOn(ExceptionBuilder, 'getExceptionBuilderComponentLazy').mockReturnValue(emptyComp); + (useFetchIndex as jest.Mock).mockImplementation(() => [ false, { @@ -77,7 +81,7 @@ describe('Event filter form', () => { it('should renders correctly with data', () => { component = renderComponentWithdata(); - expect(component.getByText(ecsEventMock().process!.executable![0])).not.toBeNull(); + expect(component.getByTestId('alert-exception-builder')).not.toBeNull(); expect(component.getByText(NAME_ERROR)).not.toBeNull(); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx index 744fb9930321d..d74baab0d2bbc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx @@ -115,26 +115,25 @@ export const EventFiltersForm: React.FC = memo( ); const exceptionBuilderComponentMemo = useMemo( - () => ( - - ), + () => + ExceptionBuilder.getExceptionBuilderComponentLazy({ + allowLargeValueLists: true, + httpService: http, + autocompleteService: data.autocomplete, + exceptionListItems: [exception as ExceptionListItemSchema], + listType: EVENT_FILTER_LIST_TYPE, + listId: ENDPOINT_EVENT_FILTERS_LIST_ID, + listNamespaceType: 'agnostic', + ruleName: RULE_NAME, + indexPatterns, + isOrDisabled: true, // TODO: pending to be validated + isAndDisabled: false, + isNestedDisabled: false, + dataTestSubj: 'alert-exception-builder', + idAria: 'alert-exception-builder', + onChange: handleOnBuilderChange, + listTypeSpecificIndexPatternFilter: filterIndexPatterns, + }), [data, handleOnBuilderChange, http, indexPatterns, exception] );