[9.0] [Security Solution] Fixes related integrations render performance on rule editing pages (#217254) (#218671)

# Backport

This will backport the following commits from `main` to `9.0`:
- [[Security Solution] Fixes related integrations render performance on
rule editing pages
(#217254)](https://github.com/elastic/kibana/pull/217254)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Davis
Plumlee","email":"56367316+dplumlee@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-04-18T18:47:20Z","message":"[Security
Solution] Fixes related integrations render performance on rule editing
pages (#217254)\n\n## Summary\n\nFixes
https://github.com/elastic/kibana/issues/183607\n\nAdds logic to fix the
re-render performance issues caused by the related\nintegrations
component on the rule edit and creation pages. This copies\na strategy
used in https://github.com/elastic/kibana/pull/180682 to fix\na similar
issue with required fields. Related integrations component now\ndoesn't
re-render when there are updates to components that don't
affect\nit.\n\n#### React Profile while typing in query field
component\n![Screenshot 2025-04-04 at 8
12\n38 PM](https://github.com/user-attachments/assets/9d3edcaa-4856-42df-9e6d-59bcc4785b5d)\n\n\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios\n\n---------\n\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"1b6376e9c9e09edb5b0e790bdccc7d14bd5801ca","branchLabelMapping":{"^v9.1.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:fix","Team:Detections
and Resp","Team: SecuritySolution","Feature:Rule
Management","Team:Detection Rule
Management","backport:version","v9.1.0","v8.19.0","v8.18.1","v9.0.1"],"title":"[Security
Solution] Fixes related integrations render performance on rule editing
pages","number":217254,"url":"https://github.com/elastic/kibana/pull/217254","mergeCommit":{"message":"[Security
Solution] Fixes related integrations render performance on rule editing
pages (#217254)\n\n## Summary\n\nFixes
https://github.com/elastic/kibana/issues/183607\n\nAdds logic to fix the
re-render performance issues caused by the related\nintegrations
component on the rule edit and creation pages. This copies\na strategy
used in https://github.com/elastic/kibana/pull/180682 to fix\na similar
issue with required fields. Related integrations component now\ndoesn't
re-render when there are updates to components that don't
affect\nit.\n\n#### React Profile while typing in query field
component\n![Screenshot 2025-04-04 at 8
12\n38 PM](https://github.com/user-attachments/assets/9d3edcaa-4856-42df-9e6d-59bcc4785b5d)\n\n\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios\n\n---------\n\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"1b6376e9c9e09edb5b0e790bdccc7d14bd5801ca"}},"sourceBranch":"main","suggestedTargetBranches":["8.19","8.18","9.0"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/217254","number":217254,"mergeCommit":{"message":"[Security
Solution] Fixes related integrations render performance on rule editing
pages (#217254)\n\n## Summary\n\nFixes
https://github.com/elastic/kibana/issues/183607\n\nAdds logic to fix the
re-render performance issues caused by the related\nintegrations
component on the rule edit and creation pages. This copies\na strategy
used in https://github.com/elastic/kibana/pull/180682 to fix\na similar
issue with required fields. Related integrations component now\ndoesn't
re-render when there are updates to components that don't
affect\nit.\n\n#### React Profile while typing in query field
component\n![Screenshot 2025-04-04 at 8
12\n38 PM](https://github.com/user-attachments/assets/9d3edcaa-4856-42df-9e6d-59bcc4785b5d)\n\n\n\n###
Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers
should verify this PR satisfies this list as well.\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios\n\n---------\n\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"1b6376e9c9e09edb5b0e790bdccc7d14bd5801ca"}},{"branch":"8.19","label":"v8.19.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.18","label":"v8.18.1","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.0","label":"v9.0.1","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Davis Plumlee <56367316+dplumlee@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2025-04-18 22:12:44 +02:00 committed by GitHub
parent f9f11a5e4b
commit 01d7f190db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 112 additions and 60 deletions

View file

@ -8,55 +8,101 @@
import React from 'react'; import React from 'react';
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui';
import { UseArray, useFormData } from '../../../../shared_imports'; import { UseArray, useFormData } from '../../../../shared_imports';
import type { FormHook, ArrayItem } from '../../../../shared_imports';
import { RelatedIntegrationsHelpInfo } from './related_integrations_help_info'; import { RelatedIntegrationsHelpInfo } from './related_integrations_help_info';
import { RelatedIntegrationFieldRow } from './related_integration_field_row'; import { RelatedIntegrationFieldRow } from './related_integration_field_row';
import * as i18n from './translations'; import * as i18n from './translations';
import { OptionalFieldLabel } from '../optional_field_label'; import { OptionalFieldLabel } from '../optional_field_label';
import { getFlattenedArrayFieldNames } from '../utils';
interface RelatedIntegrationsProps { interface RelatedIntegrationsProps {
path: string; path: string;
dataTestSubj?: string; dataTestSubj?: string;
} }
export function RelatedIntegrations({ path, dataTestSubj }: RelatedIntegrationsProps): JSX.Element { function RelatedIntegrationsComponent({
path,
dataTestSubj,
}: RelatedIntegrationsProps): JSX.Element {
return (
<UseArray path={path} initialNumberOfItems={0}>
{({ items, addItem, removeItem, form }) => (
<RelatedIntegrationsList
items={items}
addItem={addItem}
removeItem={removeItem}
path={path}
form={form}
dataTestSubj={dataTestSubj}
/>
)}
</UseArray>
);
}
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 = ( const label = (
<> <>
{i18n.RELATED_INTEGRATIONS_LABEL} {i18n.RELATED_INTEGRATIONS_LABEL}
<RelatedIntegrationsHelpInfo /> <RelatedIntegrationsHelpInfo />
</> </>
); );
const [formData] = useFormData();
return ( return (
<UseArray path={path} initialNumberOfItems={0}> <EuiFormRow
{({ items, addItem, removeItem }) => ( label={label}
<EuiFormRow labelAppend={OptionalFieldLabel}
label={label} labelType="legend"
labelAppend={OptionalFieldLabel} fullWidth
labelType="legend" data-test-subj={dataTestSubj}
fullWidth hasChildLabel={false}
data-test-subj={dataTestSubj} >
hasChildLabel={false} <>
> <EuiFlexGroup direction="column" gutterSize="s">
<> {items.map((item) => (
<EuiFlexGroup direction="column" gutterSize="s"> <EuiFlexItem key={item.id} data-test-subj="relatedIntegrationRow">
{items.map((item) => ( <RelatedIntegrationFieldRow
<EuiFlexItem key={item.id} data-test-subj="relatedIntegrationRow"> item={item}
<RelatedIntegrationFieldRow relatedIntegrations={formData[path] ?? []}
item={item} removeItem={removeItem}
relatedIntegrations={formData[path] ?? []} />
removeItem={removeItem} </EuiFlexItem>
/> ))}
</EuiFlexItem> </EuiFlexGroup>
))} {items.length > 0 && <EuiSpacer size="s" />}
</EuiFlexGroup> <EuiButtonEmpty size="xs" iconType="plusInCircle" onClick={addItem}>
{items.length > 0 && <EuiSpacer size="s" />} {i18n.ADD_INTEGRATION}
<EuiButtonEmpty size="xs" iconType="plusInCircle" onClick={addItem}> </EuiButtonEmpty>
{i18n.ADD_INTEGRATION} </>
</EuiButtonEmpty> </EuiFormRow>
</>
</EuiFormRow>
)}
</UseArray>
); );
} };
export const RelatedIntegrations = React.memo(RelatedIntegrationsComponent);

View file

@ -8,7 +8,7 @@
import type { RequiredFieldInput } from '../../../../../common/api/detection_engine/model/rule_schema/common_attributes.gen'; 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 type { ERROR_CODE, FormData, FormHook, ValidationFunc } from '../../../../shared_imports';
import * as i18n from './translations'; import * as i18n from './translations';
import { getFlattenedArrayFieldNames } from './utils'; import { getFlattenedArrayFieldNames } from '../utils';
export function makeValidateRequiredField(parentFieldPath: string) { export function makeValidateRequiredField(parentFieldPath: string) {
return function validateRequiredField( return function validateRequiredField(

View file

@ -16,7 +16,7 @@ import { RequiredFieldsHelpInfo } from './required_fields_help_info';
import * as defineRuleI18n from '../../../rule_creation_ui/components/step_define_rule/translations'; import * as defineRuleI18n from '../../../rule_creation_ui/components/step_define_rule/translations';
import { OptionalFieldLabel } from '../optional_field_label'; import { OptionalFieldLabel } from '../optional_field_label';
import { RequiredFieldRow } from './required_fields_row'; import { RequiredFieldRow } from './required_fields_row';
import { getFlattenedArrayFieldNames } from './utils'; import { getFlattenedArrayFieldNames } from '../utils';
import * as i18n from './translations'; import * as i18n from './translations';
interface RequiredFieldsComponentProps { interface RequiredFieldsComponentProps {

View file

@ -5,8 +5,6 @@
* 2.0. * 2.0.
*/ */
import type { FormHook, ArrayItem } from '../../../../shared_imports';
interface PickTypeForNameParameters { interface PickTypeForNameParameters {
name: string; name: string;
type: string; type: string;
@ -28,26 +26,3 @@ export function pickTypeForName({ name, type, typesByFieldName = {} }: PickTypeF
*/ */
return typesAvailableForName[0] ?? type; 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);
}

View file

@ -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);
}