Skip to content

Commit

Permalink
Merge branch '8.7' into backport/8.7/pr-151023
Browse files Browse the repository at this point in the history
  • Loading branch information
Thomas Watson authored Feb 17, 2023
2 parents 321e792 + ca32791 commit 10b573f
Show file tree
Hide file tree
Showing 24 changed files with 321 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Side Public License, v 1.
*/

import React, { useState, useRef, useEffect, FC } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import { EuiLoadingChart } from '@elastic/eui';
import classNames from 'classnames';

Expand Down Expand Up @@ -96,21 +96,20 @@ const Item = React.forwardRef<HTMLDivElement, Props>(
}
);

export const ObservedItem: FC<Props> = (props: Props) => {
export const ObservedItem = React.forwardRef<HTMLDivElement, Props>((props, panelRef) => {
const [intersection, updateIntersection] = useState<IntersectionObserverEntry>();
const [isRenderable, setIsRenderable] = useState(false);
const panelRef = useRef<HTMLDivElement>(null);

const observerRef = useRef(
new window.IntersectionObserver(([value]) => updateIntersection(value), {
root: panelRef.current,
root: (panelRef as React.RefObject<HTMLDivElement>).current,
})
);

useEffect(() => {
const { current: currentObserver } = observerRef;
currentObserver.disconnect();
const { current } = panelRef;
const { current } = panelRef as React.RefObject<HTMLDivElement>;

if (current) {
currentObserver.observe(current);
Expand All @@ -126,9 +125,11 @@ export const ObservedItem: FC<Props> = (props: Props) => {
}, [intersection, isRenderable]);

return <Item ref={panelRef} isRenderable={isRenderable} {...props} />;
};
});

export const DashboardGridItem: FC<Props> = (props: Props) => {
// ReactGridLayout passes ref to children. Functional component children require forwardRef to avoid react warning
// https://github.com/react-grid-layout/react-grid-layout#custom-child-components-and-draggable-handles
export const DashboardGridItem = React.forwardRef<HTMLDivElement, Props>((props, ref) => {
const {
settings: { isProjectEnabledInLabs },
} = pluginServices.getServices();
Expand All @@ -138,5 +139,5 @@ export const DashboardGridItem: FC<Props> = (props: Props) => {
const isPrintMode = select((state) => state.explicitInput.viewMode) === ViewMode.PRINT;
const isEnabled = !isPrintMode && isProjectEnabledInLabs('labs:dashboard:deferBelowFold');

return isEnabled ? <ObservedItem {...props} /> : <Item {...props} />;
};
return isEnabled ? <ObservedItem ref={ref} {...props} /> : <Item ref={ref} {...props} />;
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@ import { EuiErrorBoundary } from '@elastic/eui';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import React from 'react';
import { NotFoundPrompt } from '@kbn/shared-ux-prompt-not-found';
import { useLocation } from 'react-router-dom';
import { ApmPluginStartDeps } from '../../plugin';

export class ApmErrorBoundary extends React.Component<
export function ApmErrorBoundary({ children }: { children?: React.ReactNode }) {
const location = useLocation();
return <ErrorBoundary key={location.pathname}>{children}</ErrorBoundary>;
}

class ErrorBoundary extends React.Component<
{ children?: React.ReactNode },
{ error?: Error },
{}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
import { getAllActionMessageParams } from '../../../../../../detections/pages/detection_engine/rules/helpers';

import { RuleActionsField } from '../../../../../../detections/components/rules/rule_actions_field';
import { validateRuleActionsField } from '../../../../../../detections/containers/detection_engine/rules/validate_rule_actions_field';
import { debouncedValidateRuleActionsField } from '../../../../../../detections/containers/detection_engine/rules/validate_rule_actions_field';

const CommonUseField = getUseField({ component: Field });

Expand All @@ -61,7 +61,10 @@ const getFormSchema = (
actions: {
validations: [
{
validator: validateRuleActionsField(actionTypeRegistry),
// Debounced validator not explicitly necessary here as the `RuleActionsFormData` form doesn't exhibit the same
// behavior as the `ActionsStepRule` form outlined in https://github.com/elastic/kibana/issues/142217, however
// additional renders are prevented so using for consistency
validator: debouncedValidateRuleActionsField(actionTypeRegistry),
},
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) =
triggersActionsUi: { getActionForm },
} = useKibana().services;

// Workaround for setAlertActionsProperty being fired with prevProps when followed by setActionIdByIndex
// For details see: https://github.com/elastic/kibana/issues/142217
const [isInitializingAction, setIsInitializingAction] = useState(false);

const actions: RuleAction[] = useMemo(
() => (!isEmpty(field.value) ? (field.value as RuleAction[]) : []),
[field.value]
Expand All @@ -83,6 +87,9 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) =
const setActionIdByIndex = useCallback(
(id: string, index: number) => {
const updatedActions = [...(actions as Array<Partial<RuleAction>>)];
if (isEmpty(updatedActions[index].params)) {
setIsInitializingAction(true);
}
updatedActions[index] = deepMerge(updatedActions[index], { id });
field.setValue(updatedActions);
},
Expand All @@ -98,24 +105,30 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) =
(key: string, value: RuleActionParam, index: number) => {
// validation is not triggered correctly when actions params updated (more details in https://github.com/elastic/kibana/issues/142217)
// wrapping field.setValue in setTimeout fixes the issue above
// and triggers validation after params have been updated
setTimeout(
() =>
field.setValue((prevValue: RuleAction[]) => {
const updatedActions = [...prevValue];
updatedActions[index] = {
...updatedActions[index],
params: {
...updatedActions[index].params,
[key]: value,
},
};
return updatedActions;
}),
0
);
// and triggers validation after params have been updated, however it introduced a new issue where any additional input
// would result in the cursor jumping to the end of the text area (https://github.com/elastic/kibana/issues/149885)
const updateValue = () => {
field.setValue((prevValue: RuleAction[]) => {
const updatedActions = [...prevValue];
updatedActions[index] = {
...updatedActions[index],
params: {
...updatedActions[index].params,
[key]: value,
},
};
return updatedActions;
});
};

if (isInitializingAction) {
setTimeout(updateValue, 0);
setIsInitializingAction(false);
} else {
updateValue();
}
},
[field]
[field, isInitializingAction]
);

const actionForm = useMemo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';

import type { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public';
import { validateRuleActionsField } from '../../../containers/detection_engine/rules/validate_rule_actions_field';
import { debouncedValidateRuleActionsField } from '../../../containers/detection_engine/rules/validate_rule_actions_field';

import type { FormSchema } from '../../../../shared_imports';
import type { ActionsStepRule } from '../../../pages/detection_engine/rules/types';
Expand All @@ -21,7 +21,9 @@ export const getSchema = ({
actions: {
validations: [
{
validator: validateRuleActionsField(actionTypeRegistry),
// Debounced validator is necessary here to prevent error validation
// flashing when first adding an action. Also prevents additional renders
validator: debouncedValidateRuleActionsField(actionTypeRegistry),
},
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
*/

export { validateRuleActionsField } from './validate_rule_actions_field';
export { debouncedValidateRuleActionsField } from './validate_rule_actions_field';
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,22 @@

/* istanbul ignore file */

import type {
ValidationCancelablePromise,
ValidationFuncArg,
ValidationResponsePromise,
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import type {
RuleAction,
ActionTypeRegistryContract,
} from '@kbn/triggers-actions-ui-plugin/public';
import type { ValidationFunc, ERROR_CODE, ValidationError } from '../../../../../shared_imports';
import type { RuleActionsFormData } from '../../../../../detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/rule_actions_form';
import type { ActionsStepRule } from '../../../../pages/detection_engine/rules/types';
import type { ValidationFunc, ERROR_CODE } from '../../../../../shared_imports';
import { getActionTypeName, validateMustache, validateActionParams } from './utils';

export const DEFAULT_VALIDATION_TIMEOUT = 100;

export const validateSingleAction = async (
actionItem: RuleAction,
actionTypeRegistry: ActionTypeRegistryContract
Expand All @@ -26,9 +35,7 @@ export const validateSingleAction = async (

export const validateRuleActionsField =
(actionTypeRegistry: ActionTypeRegistryContract) =>
async (
...data: Parameters<ValidationFunc>
): Promise<ValidationError<ERROR_CODE> | void | undefined> => {
async (...data: Parameters<ValidationFunc>): ValidationResponsePromise<ERROR_CODE> => {
const [{ value, path }] = data as [{ value: RuleAction[]; path: string }];

const errors = [];
Expand All @@ -51,3 +58,40 @@ export const validateRuleActionsField =
};
}
};

/**
* Debounces validation by canceling previous validation requests. Essentially leveraging the async validation
* cancellation behavior from the hook_form_lib. Necessary to prevent error validation flashing when first adding an
* action until root cause of https://github.com/elastic/kibana/issues/142217 is found
*
* See docs for details:
* https://docs.elastic.dev/form-lib/examples/validation#cancel-asynchronous-validation
*
* Note: _.throttle/debounce does not have async support, and so not used https://github.com/lodash/lodash/issues/4815.
*
* @param actionTypeRegistry
* @param defaultValidationTimeout
*/
export const debouncedValidateRuleActionsField =
(
actionTypeRegistry: ActionTypeRegistryContract,
defaultValidationTimeout = DEFAULT_VALIDATION_TIMEOUT
) =>
(data: ValidationFuncArg<ActionsStepRule | RuleActionsFormData>): ValidationResponsePromise => {
let isCanceled = false;
const promise: ValidationCancelablePromise = new Promise((resolve) => {
setTimeout(() => {
if (isCanceled) {
resolve();
} else {
resolve(validateRuleActionsField(actionTypeRegistry)(data));
}
}, defaultValidationTimeout);
});

promise.cancel = () => {
isCanceled = true;
};

return promise;
};
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ const getIntegrationsInfoFromPolicy = (
packageInfo: InstalledPackageBasicInfo
): InstalledIntegrationBasicInfo[] => {
return policy.inputs.map((input) => {
const integrationName = normalizeString(input.policy_template); // e.g. 'cloudtrail'
const integrationName = normalizeString(input.policy_template ?? input.type); // e.g. 'cloudtrail'
const integrationTitle = `${packageInfo.package_title} ${capitalize(integrationName)}`; // e.g. 'AWS Cloudtrail'
return {
integration_name: integrationName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
*/

import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '@kbn/ml-plugin/common/constants/categorization_job';
import { FtrProviderContext } from '../../../ftr_provider_context';
import type { FtrProviderContext } from '../../../ftr_provider_context';
import type { FieldStatsType } from '../common/types';

export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
Expand Down Expand Up @@ -74,6 +75,13 @@ export default function ({ getService }: FtrProviderContext) {

const calendarId = `wizard-test-calendar_${Date.now()}`;

const fieldStatsEntries = [
{
fieldName: 'field1',
type: 'keyword' as FieldStatsType,
},
];

describe('categorization', function () {
this.tags(['ml']);
before(async () => {
Expand Down Expand Up @@ -129,8 +137,18 @@ export default function ({ getService }: FtrProviderContext) {
await ml.jobWizardCategorization.assertCategorizationDetectorTypeSelectionExists();
await ml.jobWizardCategorization.selectCategorizationDetectorType(detectorTypeIdentifier);

await ml.testExecution.logTestStep(`job creation selects the categorization field`);
await ml.testExecution.logTestStep(
'job creation opens field stats flyout from categorization field input'
);
await ml.jobWizardCategorization.assertCategorizationFieldInputExists();
for (const { fieldName, type: fieldType } of fieldStatsEntries) {
await ml.jobWizardCategorization.assertFieldStatFlyoutContentFromCategorizationFieldInputTrigger(
fieldName,
fieldType
);
}

await ml.testExecution.logTestStep(`job creation selects the categorization field`);
await ml.jobWizardCategorization.selectCategorizationField(categorizationFieldIdentifier);
await ml.jobWizardCategorization.assertCategorizationExamplesCallout(
CATEGORY_EXAMPLES_VALIDATION_STATUS.VALID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* 2.0.
*/

import { FtrProviderContext } from '../../../ftr_provider_context';
import type { FtrProviderContext } from '../../../ftr_provider_context';
import type { FieldStatsType } from '../common/types';

export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
Expand Down Expand Up @@ -71,6 +72,14 @@ export default function ({ getService }: FtrProviderContext) {

const calendarId = `wizard-test-calendar_${Date.now()}`;

const fieldStatsEntries = [
{
fieldName: '@version.keyword',
type: 'keyword' as FieldStatsType,
expectedValues: ['1'],
},
];

describe('multi metric', function () {
this.tags(['ml']);
before(async () => {
Expand Down Expand Up @@ -129,9 +138,20 @@ export default function ({ getService }: FtrProviderContext) {
}

await ml.testExecution.logTestStep(
'job creation inputs the split field and displays split cards'
'job creation opens field stats flyout from split field input'
);
await ml.jobWizardMultiMetric.assertSplitFieldInputExists();
for (const { fieldName, type: fieldType, expectedValues } of fieldStatsEntries) {
await ml.jobWizardMultiMetric.assertFieldStatFlyoutContentFromSplitFieldInputTrigger(
fieldName,
fieldType,
expectedValues
);
}

await ml.testExecution.logTestStep(
'job creation inputs the split field and displays split cards'
);
await ml.jobWizardMultiMetric.selectSplitField(splitField);

await ml.jobWizardMultiMetric.assertDetectorSplitExists(splitField);
Expand All @@ -140,8 +160,19 @@ export default function ({ getService }: FtrProviderContext) {

await ml.jobWizardCommon.assertInfluencerSelection([splitField]);

await ml.testExecution.logTestStep('job creation displays the influencer field');
await ml.testExecution.logTestStep(
'job creation opens field stats flyout from influencer field input'
);
await ml.jobWizardCommon.assertInfluencerInputExists();
for (const { fieldName, type: fieldType, expectedValues } of fieldStatsEntries) {
await ml.jobWizardCommon.assertFieldStatFlyoutContentFromInfluencerInputTrigger(
fieldName,
fieldType,
expectedValues
);
}

await ml.testExecution.logTestStep('job creation displays the influencer field');
await ml.jobWizardCommon.assertInfluencerSelection([splitField]);

await ml.testExecution.logTestStep('job creation inputs the bucket span');
Expand Down
Loading

0 comments on commit 10b573f

Please sign in to comment.