mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Security Solution] Fixes related integrations render performance on rule editing pages (#217254)
## 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  ### 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 <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
112eab3a65
commit
1b6376e9c9
5 changed files with 112 additions and 60 deletions
|
@ -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);
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue