From 1b6376e9c9e09edb5b0e790bdccc7d14bd5801ca Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Fri, 18 Apr 2025 14:47:20 -0400 Subject: [PATCH] [Security Solution] Fixes related integrations render performance on rule editing pages (#217254) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes https://github.com/elastic/kibana/issues/183607 Adds logic to fix the re-render performance issues caused by the related integrations component on the rule edit and creation pages. This copies a strategy used in https://github.com/elastic/kibana/pull/180682 to fix a similar issue with required fields. Related integrations component now doesn't re-render when there are updates to components that don't affect it. #### React Profile while typing in query field component ![Screenshot 2025-04-04 at 8 12 38 PM](https://github.com/user-attachments/assets/9d3edcaa-4856-42df-9e6d-59bcc4785b5d) ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [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 --------- Co-authored-by: Elastic Machine --- .../related_integrations.tsx | 112 ++++++++++++------ .../make_validate_required_field.ts | 2 +- .../required_fields/required_fields.tsx | 2 +- .../components/required_fields/utils.ts | 25 ---- .../rule_creation/components/utils.ts | 31 +++++ 5 files changed, 112 insertions(+), 60 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/utils.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.tsx index 8b2442940771..abce17b21a03 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/related_integrations/related_integrations.tsx @@ -8,55 +8,101 @@ import React from 'react'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { UseArray, useFormData } from '../../../../shared_imports'; +import type { FormHook, ArrayItem } from '../../../../shared_imports'; import { RelatedIntegrationsHelpInfo } from './related_integrations_help_info'; import { RelatedIntegrationFieldRow } from './related_integration_field_row'; import * as i18n from './translations'; import { OptionalFieldLabel } from '../optional_field_label'; +import { getFlattenedArrayFieldNames } from '../utils'; interface RelatedIntegrationsProps { path: string; dataTestSubj?: string; } -export function RelatedIntegrations({ path, dataTestSubj }: RelatedIntegrationsProps): JSX.Element { +function RelatedIntegrationsComponent({ + path, + dataTestSubj, +}: RelatedIntegrationsProps): JSX.Element { + return ( + + {({ items, addItem, removeItem, form }) => ( + + )} + + ); +} + +interface RelatedIntegrationsListProps { + items: ArrayItem[]; + addItem: () => void; + removeItem: (id: number) => void; + path: string; + form: FormHook; + dataTestSubj?: string; +} + +const RelatedIntegrationsList = ({ + items, + addItem, + removeItem, + path, + form, + dataTestSubj, +}: RelatedIntegrationsListProps) => { + const flattenedFieldNames = getFlattenedArrayFieldNames(form, path); + + /* + Not using "watch" for the initial render, to let row components render and initialize form fields. + Then we can use the "watch" feature to track their changes. + */ + const hasRenderedInitially = flattenedFieldNames.length > 0; + const fieldsToWatch = hasRenderedInitially ? flattenedFieldNames : []; + + const [formData] = useFormData({ watch: fieldsToWatch }); + const label = ( <> {i18n.RELATED_INTEGRATIONS_LABEL} ); - const [formData] = useFormData(); return ( - - {({ items, addItem, removeItem }) => ( - - <> - - {items.map((item) => ( - - - - ))} - - {items.length > 0 && } - - {i18n.ADD_INTEGRATION} - - - - )} - + + <> + + {items.map((item) => ( + + + + ))} + + {items.length > 0 && } + + {i18n.ADD_INTEGRATION} + + + ); -} +}; + +export const RelatedIntegrations = React.memo(RelatedIntegrationsComponent); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/make_validate_required_field.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/make_validate_required_field.ts index 26ddcc5f61c1..499f7fc60b2d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/make_validate_required_field.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/make_validate_required_field.ts @@ -8,7 +8,7 @@ import type { RequiredFieldInput } from '../../../../../common/api/detection_engine/model/rule_schema/common_attributes.gen'; import type { ERROR_CODE, FormData, FormHook, ValidationFunc } from '../../../../shared_imports'; import * as i18n from './translations'; -import { getFlattenedArrayFieldNames } from './utils'; +import { getFlattenedArrayFieldNames } from '../utils'; export function makeValidateRequiredField(parentFieldPath: string) { return function validateRequiredField( diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/required_fields.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/required_fields.tsx index 27387909d330..9bfa9d45d1bb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/required_fields.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/required_fields.tsx @@ -16,7 +16,7 @@ import { RequiredFieldsHelpInfo } from './required_fields_help_info'; import * as defineRuleI18n from '../../../rule_creation_ui/components/step_define_rule/translations'; import { OptionalFieldLabel } from '../optional_field_label'; import { RequiredFieldRow } from './required_fields_row'; -import { getFlattenedArrayFieldNames } from './utils'; +import { getFlattenedArrayFieldNames } from '../utils'; import * as i18n from './translations'; interface RequiredFieldsComponentProps { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/utils.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/utils.ts index 38820e992fa6..55beca264e12 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/utils.ts @@ -5,8 +5,6 @@ * 2.0. */ -import type { FormHook, ArrayItem } from '../../../../shared_imports'; - interface PickTypeForNameParameters { name: string; type: string; @@ -28,26 +26,3 @@ export function pickTypeForName({ name, type, typesByFieldName = {} }: PickTypeF */ return typesAvailableForName[0] ?? type; } - -/** - * Returns a list of flattened field names for a given array field of a form. - * Flattened field name is a string that represents the path to an item in an array field. - * For example, a field "myArrayField" can be represented as "myArrayField[0]", "myArrayField[1]", etc. - * - * Flattened field names are useful: - * - when you need to subscribe to changes in an array field using `useFormData` "watch" option - * - when you need to retrieve form data before serializer function is applied - * - * @param {Object} form - Form object. - * @param {string} arrayFieldName - Path to the array field. - * @returns {string[]} - Flattened array field names. - */ -export function getFlattenedArrayFieldNames( - form: { getFields: FormHook['getFields'] }, - arrayFieldName: string -): string[] { - const internalField = form.getFields()[`${arrayFieldName}__array__`] ?? {}; - const internalFieldValue = (internalField?.value ?? []) as ArrayItem[]; - - return internalFieldValue.map((item) => item.path); -} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/utils.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/utils.ts new file mode 100644 index 000000000000..79ea06bef38c --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/utils.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ArrayItem, FormHook } from '../../../shared_imports'; + +/** + * Returns a list of flattened field names for a given array field of a form. + * Flattened field name is a string that represents the path to an item in an array field. + * For example, a field "myArrayField" can be represented as "myArrayField[0]", "myArrayField[1]", etc. + * + * Flattened field names are useful: + * - when you need to subscribe to changes in an array field using `useFormData` "watch" option + * - when you need to retrieve form data before serializer function is applied + * + * @param {Object} form - Form object. + * @param {string} arrayFieldName - Path to the array field. + * @returns {string[]} - Flattened array field names. + */ +export function getFlattenedArrayFieldNames( + form: { getFields: FormHook['getFields'] }, + arrayFieldName: string +): string[] { + const internalField = form.getFields()[`${arrayFieldName}__array__`] ?? {}; + const internalFieldValue = (internalField?.value ?? []) as ArrayItem[]; + + return internalFieldValue.map((item) => item.path); +}